Compare commits
4
Commits
main
..
dev/v1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3b0fc0038 | ||
|
|
695364c2df | ||
|
|
3298111560 | ||
|
|
3ca9f54de0 |
No files matched your search
@@ -1 +0,0 @@
|
||||
.gitea/CODEOWNERS linguist-detectable=false
|
||||
@@ -1,3 +0,0 @@
|
||||
# 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
|
||||
@@ -6,22 +6,10 @@ All notable changes to this project will be documented in this file. For commit
|
||||
|
||||
**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
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://goreportcard.com/report/github.com/gtsteffaniak/filebrowser/backend)
|
||||
[](https://www.codacy.com/gh/gtsteffaniak/filebrowser/dashboard)
|
||||
[](https://github.com/gtsteffaniak/filebrowser/releases)
|
||||
[](https://github.com/gtsteffaniak/filebrowser/releases)
|
||||
[](https://hub.docker.com/r/gtstef/filebrowser)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
|
||||
+2
-2
@@ -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", "preview/thumbnails", "preview/heic"]
|
||||
exclude_dir = ["tmp", "vendor", "http/dist", "http/embed", "swagger/docs"]
|
||||
log = "air.log"
|
||||
delay = 1000 # ms
|
||||
|
||||
@@ -14,4 +14,4 @@ tmp_dir = "tmp"
|
||||
time = true
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
clean_on_exit = true
|
||||
@@ -18,4 +18,4 @@ tmp_dir = "tmp"
|
||||
time = true
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
clean_on_exit = true
|
||||
@@ -1,138 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -550,12 +550,17 @@ 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)
|
||||
}
|
||||
|
||||
@@ -575,20 +580,15 @@ 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 {
|
||||
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 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 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,6 +601,12 @@ 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)
|
||||
}
|
||||
|
||||
@@ -631,31 +637,9 @@ 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.
|
||||
// Use trimmed header to avoid false negatives from truncated sequences
|
||||
if len(trimmedHeader) > 0 && !utf8.Valid(trimmedHeader) {
|
||||
if !utf8.Valid(actualHeader) {
|
||||
return "", nil // Not an error, just not the text file we want
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
@@ -32,14 +31,11 @@ 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)
|
||||
// If parent directory doesn't exist and createUserDir is enabled, create it
|
||||
// validate that scope path exists
|
||||
if !Exists(parentDir) {
|
||||
if err := MakeUserDir(parentDir); err != nil {
|
||||
return fmt.Errorf("MakeUserDirs: failed to create parent scope directory: %s - %v", scope.Scope, err)
|
||||
}
|
||||
return fmt.Errorf("MakeUserDirs: scope path does not exist: %s", scope.Scope)
|
||||
}
|
||||
// Use JoinPathAsUnix to ensure scope remains in Unix format (forward slashes)
|
||||
scope.Scope = utils.JoinPathAsUnix(scope.Scope, cleanedUserName)
|
||||
scope.Scope = filepath.Join(scope.Scope, cleanedUserName)
|
||||
err := MakeUserDir(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MakeUserDirs: failed to create user home dir: %s", err)
|
||||
|
||||
@@ -62,13 +62,6 @@ 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 {
|
||||
@@ -82,8 +75,8 @@ func copySingleFile(source, dest string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the destination file with source permissions
|
||||
dst, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, sourcePerms)
|
||||
// Create the destination file.
|
||||
dst, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, PermFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -95,13 +88,11 @@ func copySingleFile(source, dest string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve source file permissions
|
||||
// Handle chmod errors gracefully (e.g., in rootless containers where chmod may be restricted)
|
||||
err = os.Chmod(dest, sourcePerms)
|
||||
// Set the configured file permissions instead of copying from source
|
||||
err = os.Chmod(dest, PermFile)
|
||||
|
||||
if err != nil {
|
||||
// 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 err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -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. Note: docker must also mount a persistent volume to persist the cache (default: true)
|
||||
CacheDirCleanup *bool `json:"cacheDirCleanup"` // whether to automatically cleanup the cache directory (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
|
||||
|
||||
@@ -25,7 +25,6 @@ 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"
|
||||
|
||||
+25
-51
@@ -8,7 +8,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -49,14 +48,13 @@ type duplicatesOptions struct {
|
||||
|
||||
// duplicatesHandler handles requests to find duplicate files
|
||||
//
|
||||
// This endpoint finds files with the same size, uses fuzzy filename matching
|
||||
// for initial grouping (fast), then verifies all final groups with checksums.
|
||||
// This endpoint finds files with the same size and uses fuzzy filename matching
|
||||
// to verify potential duplicates.
|
||||
//
|
||||
// It's optimized to handle large directories efficiently by:
|
||||
// 1. Filtering by minimum size first
|
||||
// 2. Grouping by size
|
||||
// 3. Initial grouping with fuzzy filename similarity (fast)
|
||||
// 4. Final verification with checksums on all groups (accurate)
|
||||
// 3. Verifying matches with fuzzy filename similarity
|
||||
//
|
||||
// @Summary Find Duplicate Files
|
||||
// @Description Finds duplicate files based on size and fuzzy filename matching
|
||||
@@ -81,8 +79,7 @@ func duplicatesHandler(w http.ResponseWriter, r *http.Request, d *requestContext
|
||||
}
|
||||
|
||||
// Generate cache key from all input parameters that affect results
|
||||
// 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)
|
||||
cacheKey := fmt.Sprintf("%s:%s:%d:%t", index.Path, opts.combinedPath, opts.minSize, opts.useChecksum)
|
||||
|
||||
// Check cache first (before acquiring mutex)
|
||||
if cachedResults, ok := duplicateResultsCache.Get(cacheKey); ok {
|
||||
@@ -135,12 +132,8 @@ func findDuplicatesInIndex(index *indexing.Index, opts *duplicatesOptions) []dup
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
}{}
|
||||
// Step 2: Process each size group to find duplicates
|
||||
duplicateGroups := []duplicateGroup{}
|
||||
totalFiles := 0
|
||||
|
||||
for size, locations := range sizeGroups {
|
||||
@@ -148,10 +141,14 @@ func findDuplicatesInIndex(index *indexing.Index, opts *duplicatesOptions) []dup
|
||||
continue // Skip files with no potential duplicates
|
||||
}
|
||||
|
||||
// Use filename matching for initial grouping (fast)
|
||||
groups := groupLocationsByFilename(locations, index, size)
|
||||
var groups [][]fileLocation
|
||||
if opts.useChecksum {
|
||||
groups = groupLocationsByChecksum(locations, index, size)
|
||||
} else {
|
||||
groups = groupLocationsByFilename(locations, index, size)
|
||||
}
|
||||
|
||||
// Collect candidate groups up to the limit
|
||||
// Step 3: Only now create SearchResult objects for actual duplicates
|
||||
for _, locGroup := range groups {
|
||||
if len(locGroup) < 2 {
|
||||
continue
|
||||
@@ -162,31 +159,7 @@ func findDuplicatesInIndex(index *indexing.Index, opts *duplicatesOptions) []dup
|
||||
break
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Create SearchResult objects only for these confirmed duplicates
|
||||
resultGroup := make([]*indexing.SearchResult, 0, len(locGroup))
|
||||
index.ReadOnlyOperation(func() {
|
||||
dirs := index.GetDirectories()
|
||||
@@ -202,7 +175,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 != candidate.size {
|
||||
if file.Size != size {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -227,18 +200,19 @@ func findDuplicatesInIndex(index *indexing.Index, opts *duplicatesOptions) []dup
|
||||
|
||||
if len(resultGroup) >= 2 {
|
||||
duplicateGroups = append(duplicateGroups, duplicateGroup{
|
||||
Size: candidate.size,
|
||||
Size: size,
|
||||
Count: len(resultGroup),
|
||||
Files: resultGroup,
|
||||
})
|
||||
totalFiles += len(resultGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort groups by size (largest to smallest)
|
||||
sort.Slice(duplicateGroups, func(i, j int) bool {
|
||||
return duplicateGroups[i].Size > duplicateGroups[j].Size
|
||||
})
|
||||
// Stop processing more size groups if we've hit the limit
|
||||
if totalFiles >= maxTotalFiles {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return duplicateGroups
|
||||
}
|
||||
@@ -247,9 +221,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")
|
||||
// Checksums are always enabled by default for final verification
|
||||
// First pass uses filename matching for speed, then checksums verify final groups
|
||||
useChecksum := true
|
||||
// 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"
|
||||
|
||||
// Default minimum size: 1MB
|
||||
minSizeMb := int64(1)
|
||||
|
||||
@@ -126,7 +126,7 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
link.IncrementUserDownload(data.user.Username)
|
||||
}
|
||||
}
|
||||
file.Path = utils.AddTrailingSlashIfNotExists(path)
|
||||
file.Path = "/" + strings.TrimPrefix(strings.TrimPrefix(file.Path, link.Path), "/")
|
||||
// Set the file info in the `data` object
|
||||
data.fileInfo = *file
|
||||
// Call the next handler with the data
|
||||
@@ -395,16 +395,6 @@ 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
|
||||
|
||||
+10
-57
@@ -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(r, source, providedPath, d.fileInfo.Hash, d.token)
|
||||
downloadURL := buildOnlyOfficeDownloadURL(source, providedPath, d.fileInfo.Hash, d.token)
|
||||
|
||||
// Build callback URL for OnlyOffice to notify us of changes
|
||||
callbackURL := buildOnlyOfficeCallbackURL(r, source, providedPath, d.fileInfo.Hash, d.token)
|
||||
callbackURL := buildOnlyOfficeCallbackURL(source, providedPath, d.fileInfo.Hash, d.token)
|
||||
|
||||
// Build OnlyOffice client configuration
|
||||
clientConfig := map[string]interface{}{
|
||||
@@ -228,37 +228,14 @@ 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(r *http.Request, source, path, hash, token string) string {
|
||||
func buildOnlyOfficeDownloadURL(source, path, hash, token string) string {
|
||||
// Determine base URL (internal URL takes priority for OnlyOffice server communication)
|
||||
var baseURL string
|
||||
baseURL := settings.Config.Server.BaseURL
|
||||
if settings.Config.Server.InternalUrl != "" {
|
||||
// InternalUrl is a full URL (e.g., http://localhost:8080), so use it directly
|
||||
// Ensure proper URL joining without double slashes
|
||||
internalURL := strings.TrimSuffix(settings.Config.Server.InternalUrl, "/")
|
||||
baseURLPath := strings.TrimPrefix(settings.Config.Server.BaseURL, "/")
|
||||
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)
|
||||
baseURL = internalURL + "/" + baseURLPath
|
||||
}
|
||||
|
||||
var downloadURL string
|
||||
@@ -278,37 +255,13 @@ func buildOnlyOfficeDownloadURL(r *http.Request, source, path, hash, token strin
|
||||
}
|
||||
|
||||
// buildOnlyOfficeCallbackURL constructs the callback URL that OnlyOffice server will use to notify us of changes
|
||||
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
|
||||
func buildOnlyOfficeCallbackURL(source, path, hash, token string) string {
|
||||
baseURL := settings.Config.Server.BaseURL
|
||||
if settings.Config.Server.InternalUrl != "" {
|
||||
// InternalUrl is a full URL (e.g., http://localhost:8080), so use it directly
|
||||
// Ensure proper URL joining without double slashes
|
||||
internalURL := strings.TrimSuffix(settings.Config.Server.InternalUrl, "/")
|
||||
baseURLPath := strings.TrimPrefix(settings.Config.Server.BaseURL, "/")
|
||||
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)
|
||||
baseURL = internalURL + "/" + baseURLPath
|
||||
}
|
||||
|
||||
var callbackURL string
|
||||
|
||||
+13
-50
@@ -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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
_, err = files.FileInfoFaster(utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,23 +158,6 @@ 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)
|
||||
@@ -339,19 +322,6 @@ 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
|
||||
@@ -510,12 +480,6 @@ 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)
|
||||
@@ -548,7 +512,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -382,12 +382,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
}
|
||||
|
||||
fileInfo, err := files.FileInfoFaster(fileOpts, accessStore)
|
||||
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
|
||||
if err != nil {
|
||||
preview.DelThumbs(r.Context(), *fileInfo)
|
||||
}
|
||||
err = files.WriteFile(fileOpts.Source, fileOpts.Path, r.Body)
|
||||
|
||||
+17
-30
@@ -26,9 +26,10 @@ 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"`
|
||||
PathExists bool `json:"pathExists"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// convertToFrontendShareResponse converts shares to response format with usernames
|
||||
@@ -62,15 +63,14 @@ 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,
|
||||
PathExists: pathExists,
|
||||
Link: s,
|
||||
Source: sourceInfo.Name, // Override to show source name instead of backend path
|
||||
Username: username,
|
||||
DownloadURL: getDownloadURL(r, s.Hash),
|
||||
PathExists: pathExists,
|
||||
})
|
||||
}
|
||||
return responses, nil
|
||||
@@ -392,7 +392,6 @@ 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.
|
||||
@@ -510,8 +509,7 @@ func shareDirectDownloadHandler(w http.ResponseWriter, r *http.Request, d *reque
|
||||
response := DirectDownloadResponse{
|
||||
Status: "201",
|
||||
Hash: existing.Hash,
|
||||
DownloadURL: getShareURL(r, existing.Hash, true),
|
||||
ShareURL: getShareURL(r, existing.Hash, false),
|
||||
DownloadURL: getDownloadURL(r, existing.Hash),
|
||||
}
|
||||
return renderJSON(w, r, response)
|
||||
}
|
||||
@@ -542,26 +540,21 @@ func shareDirectDownloadHandler(w http.ResponseWriter, r *http.Request, d *reque
|
||||
response := DirectDownloadResponse{
|
||||
Status: "200",
|
||||
Hash: secureHash,
|
||||
DownloadURL: getShareURL(r, secureHash, true),
|
||||
ShareURL: getShareURL(r, secureHash, false),
|
||||
DownloadURL: getDownloadURL(r, secureHash),
|
||||
}
|
||||
|
||||
return renderJSON(w, r, response)
|
||||
}
|
||||
|
||||
func getShareURL(r *http.Request, hash string, isDirectDownload bool) string {
|
||||
var shareURL string
|
||||
func getDownloadURL(r *http.Request, hash string) string {
|
||||
var downloadURL string
|
||||
if config.Server.ExternalUrl != "" {
|
||||
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)
|
||||
}
|
||||
|
||||
downloadURL = fmt.Sprintf("%s%spublic/api/raw?hash=%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
|
||||
@@ -575,13 +568,9 @@ func getShareURL(r *http.Request, hash string, isDirectDownload bool) string {
|
||||
host = r.Host
|
||||
scheme = getScheme(r)
|
||||
}
|
||||
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)
|
||||
}
|
||||
downloadURL = fmt.Sprintf("%s://%s%spublic/api/raw?hash=%s", scheme, host, config.Server.BaseURL, hash)
|
||||
}
|
||||
return shareURL
|
||||
return downloadURL
|
||||
}
|
||||
|
||||
// shareInfoHandler retrieves share information by hash.
|
||||
@@ -601,8 +590,6 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -233,15 +233,11 @@ 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: size,
|
||||
Size: dirInfo.Size(),
|
||||
ModTime: dirInfo.ModTime(),
|
||||
},
|
||||
}
|
||||
@@ -380,9 +376,8 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
idx.incrementScannerDirs()
|
||||
}
|
||||
} else {
|
||||
realFilePath := realPath + "/" + file.Name()
|
||||
size, shouldCountSize := idx.handleFile(file, fullCombined, realFilePath)
|
||||
itemInfo.DetectType(realFilePath, false)
|
||||
size, shouldCountSize := idx.handleFile(file, fullCombined)
|
||||
itemInfo.DetectType(realPath+"/"+file.Name(), false)
|
||||
// Set HasPreview flags - use cached metadata optimization only when indexing is enabled
|
||||
usedCachedPreview := false
|
||||
if !idx.Config.DisableIndexing && config.Recursive {
|
||||
@@ -763,6 +758,43 @@ 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, "./") {
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
package indexing
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
func CheckWindowsHidden(realpath string) bool {
|
||||
@@ -27,75 +24,14 @@ func getPartitionSize(path string) (uint64, error) {
|
||||
return total, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
return 0, 1, 0, false
|
||||
}
|
||||
|
||||
// platform specific rules
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
package indexing
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
@@ -47,27 +46,13 @@ func getPartitionSize(path string) (uint64, error) {
|
||||
return totalBytes, nil
|
||||
}
|
||||
|
||||
func getFileDetails(sys any, filePath string) (uint64, uint64, uint64, bool) {
|
||||
func getFileDetails(sys any) (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, "\\")
|
||||
|
||||
@@ -276,9 +276,7 @@ func TestService_Resize(t *testing.T) {
|
||||
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Use a temporary directory for cache to avoid creating directories in the source tree
|
||||
tmpDir := t.TempDir()
|
||||
svc := NewPreviewGenerator(1, tmpDir)
|
||||
svc := NewPreviewGenerator(1, "")
|
||||
source := test.source(t)
|
||||
defer source.Close()
|
||||
|
||||
@@ -436,9 +434,7 @@ func TestService_FormatFromExtension(t *testing.T) {
|
||||
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Use a temporary directory for cache to avoid creating directories in the source tree
|
||||
tmpDir := t.TempDir()
|
||||
svc := NewPreviewGenerator(1, tmpDir)
|
||||
svc := NewPreviewGenerator(1, "")
|
||||
got, err := svc.FormatFromExtension(test.ext)
|
||||
require.Truef(t, errors.Is(err, test.wantErr), "error = %v, wantErr %v", err, test.wantErr)
|
||||
if err != nil {
|
||||
|
||||
@@ -30,7 +30,6 @@ 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
|
||||
@@ -45,43 +44,36 @@ 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(actualCacheDir)
|
||||
fileCache, err = diskcache.NewFileCache(cacheDir)
|
||||
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 using the determined cache directory
|
||||
err = os.MkdirAll(filepath.Join(actualCacheDir, "thumbnails", "docs"), fileutils.PermDir)
|
||||
// Create directories recursively
|
||||
err = os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "docs"), fileutils.PermDir)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
err = os.MkdirAll(filepath.Join(actualCacheDir, "thumbnails", "videos"), fileutils.PermDir)
|
||||
err = os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "videos"), fileutils.PermDir)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
err = os.MkdirAll(filepath.Join(actualCacheDir, "heic"), fileutils.PermDir)
|
||||
err = os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "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(actualCacheDir, "heic"))
|
||||
imageService := ffmpeg.NewFFmpegService(concurrencyLimit, settings.Config.Integrations.Media.Debug, filepath.Join(settings.Config.Server.CacheDir, "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),
|
||||
@@ -187,7 +179,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(service.cacheDir, "thumbnails", "docs", hash) + ".txt"
|
||||
tempFilePath := filepath.Join(settings.Config.Server.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)
|
||||
|
||||
@@ -2917,9 +2917,6 @@ const docTemplate = `{
|
||||
"hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -3066,9 +3063,6 @@ const docTemplate = `{
|
||||
"description": "type of share: normal, upload, max",
|
||||
"type": "string"
|
||||
},
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -3850,7 +3844,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"cacheDirCleanup": {
|
||||
"description": "whether to automatically cleanup the cache directory. Note: docker must also mount a persistent volume to persist the cache (default: true)",
|
||||
"description": "whether to automatically cleanup the cache directory (default: true)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"database": {
|
||||
@@ -4354,9 +4348,6 @@ const docTemplate = `{
|
||||
"description": "type of share: normal, upload, max",
|
||||
"type": "string"
|
||||
},
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4487,9 +4478,6 @@ const docTemplate = `{
|
||||
"description": "type of share: normal, upload, max",
|
||||
"type": "string"
|
||||
},
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4626,9 +4614,6 @@ const docTemplate = `{
|
||||
"description": "type of share: normal, upload, max",
|
||||
"type": "string"
|
||||
},
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
|
||||
@@ -2906,9 +2906,6 @@
|
||||
"hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"shareUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -3055,9 +3052,6 @@
|
||||
"description": "type of share: normal, upload, max",
|
||||
"type": "string"
|
||||
},
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -3839,7 +3833,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"cacheDirCleanup": {
|
||||
"description": "whether to automatically cleanup the cache directory. Note: docker must also mount a persistent volume to persist the cache (default: true)",
|
||||
"description": "whether to automatically cleanup the cache directory (default: true)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"database": {
|
||||
@@ -4343,9 +4337,6 @@
|
||||
"description": "type of share: normal, upload, max",
|
||||
"type": "string"
|
||||
},
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4476,9 +4467,6 @@
|
||||
"description": "type of share: normal, upload, max",
|
||||
"type": "string"
|
||||
},
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
@@ -4615,9 +4603,6 @@
|
||||
"description": "type of share: normal, upload, max",
|
||||
"type": "string"
|
||||
},
|
||||
"shareURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"sidebarLinks": {
|
||||
"description": "customizable sidebar links",
|
||||
"type": "array",
|
||||
|
||||
@@ -16,8 +16,6 @@ definitions:
|
||||
properties:
|
||||
hash:
|
||||
type: string
|
||||
shareUrl:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
url:
|
||||
@@ -117,8 +115,6 @@ definitions:
|
||||
shareType:
|
||||
description: 'type of share: normal, upload, max'
|
||||
type: string
|
||||
shareURL:
|
||||
type: string
|
||||
sidebarLinks:
|
||||
description: customizable sidebar links
|
||||
items:
|
||||
@@ -699,8 +695,7 @@ definitions:
|
||||
files
|
||||
type: string
|
||||
cacheDirCleanup:
|
||||
description: 'whether to automatically cleanup the cache directory. Note:
|
||||
docker must also mount a persistent volume to persist the cache (default:
|
||||
description: 'whether to automatically cleanup the cache directory (default:
|
||||
true)'
|
||||
type: boolean
|
||||
database:
|
||||
@@ -1078,8 +1073,6 @@ definitions:
|
||||
shareType:
|
||||
description: 'type of share: normal, upload, max'
|
||||
type: string
|
||||
shareURL:
|
||||
type: string
|
||||
sidebarLinks:
|
||||
description: customizable sidebar links
|
||||
items:
|
||||
@@ -1171,8 +1164,6 @@ definitions:
|
||||
shareType:
|
||||
description: 'type of share: normal, upload, max'
|
||||
type: string
|
||||
shareURL:
|
||||
type: string
|
||||
sidebarLinks:
|
||||
description: customizable sidebar links
|
||||
items:
|
||||
@@ -1268,8 +1259,6 @@ definitions:
|
||||
shareType:
|
||||
description: 'type of share: normal, upload, max'
|
||||
type: string
|
||||
shareURL:
|
||||
type: string
|
||||
sidebarLinks:
|
||||
description: customizable sidebar links
|
||||
items:
|
||||
|
||||
@@ -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. Note: docker must also mount a persistent volume to persist the cache (default: true)
|
||||
cacheDirCleanup: true # whether to automatically cleanup the cache directory (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
|
||||
|
||||
@@ -81,7 +81,7 @@ export async function put(source, path, content = '') {
|
||||
return await resourceAction(source, path, 'PUT', content)
|
||||
}
|
||||
|
||||
export async function download(format, files, shareHash = "") {
|
||||
export function download(format, files, shareHash = "") {
|
||||
if (format !== 'zip') {
|
||||
format = 'tar.gz'
|
||||
}
|
||||
@@ -102,53 +102,13 @@ export async function download(format, files, shareHash = "") {
|
||||
})
|
||||
const url = window.origin + apiPath
|
||||
|
||||
|
||||
// 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)
|
||||
// Create a temporary <a> element to trigger the download
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.setAttribute('download', filename)
|
||||
link.href = url
|
||||
link.setAttribute('download', '') // Ensures it triggers a download
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
// Clean up the blob URL
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
|
||||
document.body.removeChild(link) // Clean up
|
||||
}
|
||||
|
||||
export function post(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getPublicApiPath, encodedPath, doubleEncode } from "@/utils/url.js";
|
||||
import { getApiPath, getPublicApiPath, encodedPath, doubleEncode } from "@/utils/url.js";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
import { state } from "@/store";
|
||||
|
||||
// ============================================================================
|
||||
@@ -91,6 +92,19 @@ 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,
|
||||
|
||||
@@ -78,7 +78,6 @@ 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 {
|
||||
@@ -359,7 +358,6 @@ 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(
|
||||
@@ -442,24 +440,9 @@ export default {
|
||||
} else {
|
||||
await filesApi.moveCopy(items, "move", overwrite, rename);
|
||||
}
|
||||
// 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
|
||||
// Close the prompt after successful operation
|
||||
notify.showSuccess(this.$t("prompts.moveSuccess"));
|
||||
mutations.closeHovers();
|
||||
mutations.setReload(true);
|
||||
} catch (error) {
|
||||
// Close the prompt and let error handling continue
|
||||
mutations.closeHovers();
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
<strong>{{ $t("prompts.size", { suffix: ":" }) }}</strong> {{ humanSize }}
|
||||
<!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
</div>
|
||||
<div v-if="!shareInfo.disableDownload" class="share__box__element share__box__center">
|
||||
<button class="button button--flat clickable" @click="goToDownload()"> {{ $t("general.download") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,6 +87,7 @@ export default {
|
||||
},
|
||||
confirm() {
|
||||
this.onConfirm();
|
||||
mutations.setReload(true);
|
||||
mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -188,11 +188,10 @@ export default {
|
||||
// Sets the current path and resets
|
||||
// the current items.
|
||||
this.current = req.path;
|
||||
this.source = req.source || null; // For shares, source might be null/undefined
|
||||
this.source = req.source;
|
||||
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
|
||||
|
||||
@@ -122,8 +122,12 @@ export default {
|
||||
return this.destPath.startsWith(itemPath + "/") || this.destPath === itemPath;
|
||||
},
|
||||
canCreateFolder() {
|
||||
const perms = getters.permissions();
|
||||
return !!perms?.create;
|
||||
// Detect correct permission for shares for show the new folder button.
|
||||
if (getters.isShare()) {
|
||||
return state.shareInfo?.allowCreate;
|
||||
} else {
|
||||
return state.user?.permissions?.modify;
|
||||
}
|
||||
},
|
||||
closeHovers() {
|
||||
return mutations.closeHovers();
|
||||
@@ -312,17 +316,14 @@ 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 (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, {});
|
||||
if (destSource && destPath) {
|
||||
goToItem(destSource, destPath, {});
|
||||
}
|
||||
};
|
||||
const buttonProps = {
|
||||
icon: "folder",
|
||||
buttons: destPath ? [
|
||||
buttons: destSource && destPath ? [
|
||||
{
|
||||
label: this.$t("buttons.goToItem"),
|
||||
primary: true,
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
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: {
|
||||
@@ -52,14 +51,16 @@ 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 ext = getFileExtension(this.item.name);
|
||||
const filenamePrefix = this.item.name.substring(0, this.item.name.length - ext.length);
|
||||
const lastDotIndex = this.item.name.lastIndexOf('.');
|
||||
const hasExtension = lastDotIndex > 0;
|
||||
return {
|
||||
fileName: filenamePrefix,
|
||||
fileExtension: removePrefix(ext, "."),
|
||||
name: itemName, // Initialize name for non-directory items
|
||||
fileName: hasExtension ? itemName.substring(0, lastDotIndex) : itemName,
|
||||
fileExtension: hasExtension ? itemName.substring(lastDotIndex + 1) : "",
|
||||
name: itemName,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -107,8 +108,7 @@ export default {
|
||||
* @param {string} value
|
||||
*/
|
||||
validateFileName(value) {
|
||||
// Handle undefined, null, or empty values
|
||||
if (!value || value === "") {
|
||||
if (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 && item.name.toLowerCase() === value.toLowerCase()) {
|
||||
if (item.name.toLowerCase() === value.toLowerCase()) {
|
||||
return { valid: false, reason: 'conflict' };
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,7 @@ export default {
|
||||
}
|
||||
|
||||
.extension-input {
|
||||
width: 6em;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.extension-separator {
|
||||
|
||||
@@ -55,14 +55,14 @@
|
||||
</button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button class="action copy-clipboard" :data-clipboard-text="link.shareURL"
|
||||
<button class="action copy-clipboard" :data-clipboard-text="buildLink(link)"
|
||||
:aria-label="$t('buttons.copyToClipboard')" :title="$t('buttons.copyToClipboard')">
|
||||
<i class="material-icons">content_paste</i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<td class="small" v-if="hasDownloadLink()">
|
||||
<button :disabled="link.shareType == 'upload'" class="action copy-clipboard"
|
||||
:data-clipboard-text="link.downloadURL" :aria-label="$t('buttons.copyDownloadLinkToClipboard')"
|
||||
:data-clipboard-text="buildDownloadLink(link)" :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 { shareApi } from "@/api";
|
||||
import { publicApi, shareApi } from "@/api";
|
||||
import Clipboard from "clipboard";
|
||||
import { fromNow } from "@/utils/moment";
|
||||
import { buildItemUrl } from "@/utils/url";
|
||||
import { buildItemUrl, fixDownloadURL } from "@/utils/url";
|
||||
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
|
||||
import SettingsItem from "@/components/settings/SettingsItem.vue";
|
||||
import FileList from "./FileList.vue";
|
||||
@@ -746,6 +746,31 @@ 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;
|
||||
@@ -791,6 +816,9 @@ export default {
|
||||
];
|
||||
}
|
||||
},
|
||||
fixDownloadURL(downloadUrl) {
|
||||
return fixDownloadURL(downloadUrl);
|
||||
},
|
||||
/**
|
||||
* @param {{path: string, source: string}} pathOrData
|
||||
*/
|
||||
|
||||
@@ -97,7 +97,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getShareLink() {
|
||||
return state.shareInfo.shareURL;
|
||||
return publicApi.getShareURL({
|
||||
hash: state.share.hash,
|
||||
});
|
||||
},
|
||||
close() {
|
||||
mutations.closeHovers();
|
||||
|
||||
@@ -341,21 +341,6 @@ 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");
|
||||
@@ -391,11 +376,7 @@ export default {
|
||||
if (this.isNew && this.availableSources.length) {
|
||||
const newSource = this.availableSources.shift();
|
||||
if (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.selectedSources.push(newSource);
|
||||
this.emitUserUpdate();
|
||||
}
|
||||
}
|
||||
@@ -407,25 +388,18 @@ 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: scopesToSend });
|
||||
await usersApi.create({ ...this.user, scopes: this.selectedSources });
|
||||
// Emit event to refresh user list
|
||||
eventBus.emit('usersChanged');
|
||||
// Close the modal
|
||||
mutations.closeHovers();
|
||||
} else {
|
||||
await usersApi.update({ ...this.user, scopes: scopesToSend }, fields);
|
||||
await usersApi.update({ ...this.user, scopes: this.selectedSources }, 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');
|
||||
|
||||
@@ -274,8 +274,7 @@ body.rtl #listingView {
|
||||
#listingView .header .duration,
|
||||
#listingView.list .listing-item .duration,
|
||||
#listingView.compact .listing-item .duration {
|
||||
margin-left: auto;
|
||||
padding-right: 1em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#listingView.list .listing-item .size.adjustment,
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"download": "تنزيل الملفات"
|
||||
},
|
||||
"permissions-name": "الصلاحيات",
|
||||
"permissionsHelp": "يمكنك تعيين المستخدم كـ \"مدير\" أو تحديد الصلاحيات بشكل منفرد.\n إذا قمت بتحديد المستخدم كـ \"مدير\"، باقي الخيارات سيتم تحديدها تلقائياً.\n إدارة المستخدمين تبقى صلاحية فريدة للـ \"مدير\" فقط.\n",
|
||||
"profileSettings": "إعدادات الحساب",
|
||||
"ruleExample1": "منع الوصول إلى الملفات التي تبدأ بنقطة مثل (.git، و .gitignore) في كل مجلد.\n",
|
||||
"ruleExample2": "منع الوصول إلى الملف المسمى Caddyfile في نطاق الجذر.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "الوكيل"
|
||||
},
|
||||
"newUserHintSubFolder": "على سبيل المثال '/مجلد فرعي' - فارغ للافتراضي",
|
||||
"permissionsHelp": "تتحكم أذونات المستخدم في الوصول إلى نوع الإجراءات التي يمكن للمستخدم تنفيذها. يمنح الوصول إلى المسؤول التحكم الكامل لتحديث وتغيير إعدادات أي مستخدم بالإضافة إلى العديد من إعدادات النظام."
|
||||
"newUserHintSubFolder": "على سبيل المثال '/مجلد فرعي' - فارغ للافتراضي"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "إعدادات الموقع",
|
||||
|
||||
@@ -426,6 +426,7 @@
|
||||
"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.",
|
||||
@@ -453,8 +454,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "např. '/subfolder' - prázdný pro výchozí nastavení"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Nastavení webu",
|
||||
|
||||
@@ -276,6 +276,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "z.B. '/subfolder' - leer für Standard"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Seiteneinstellungen",
|
||||
|
||||
@@ -282,6 +282,7 @@
|
||||
"download": "Λήψη αρχείων"
|
||||
},
|
||||
"permissions-name": "Δικαιώματα",
|
||||
"permissionsHelp": "Μπορείτε να ορίσετε τον χρήστη ως διαχειριστή ή να επιλέξετε τα δικαιώματα μεμονωμένα. Αν επιλέξετε \"Διαχειριστής\", όλες οι υπόλοιπες επιλογές θα είναι αυτόματα επιλεγμένες. Η διαχείριση χρηστών παραμένει προνόμιο ενός χρήστη με τον ρόλο του διαχειριστή.\n",
|
||||
"profileSettings": "Ρυθμίσεις προφίλ",
|
||||
"ruleExample1": "αποκλείει την πρόσβαση σε οποιοδήποτε κρυφό αρχείο (όπως .git, .gitignore) σε κάθε φάκελο.\n",
|
||||
"ruleExample2": "αποκλείει την πρόσβαση στο αρχείο με το όνομα Caddyfile στον ριζικό φάκελο της εμβέλειας του κανόνα.",
|
||||
@@ -325,8 +326,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Πληρεξούσιος"
|
||||
},
|
||||
"newUserHintSubFolder": "π.χ. '/subfolder' - κενό για προεπιλογή",
|
||||
"permissionsHelp": "Τα δικαιώματα χρήστη ελέγχουν την πρόσβαση σε είδος ενεργειών που μπορεί να εκτελέσει ο χρήστης. Η πρόσβαση διαχειριστή παρέχει πλήρη έλεγχο για την ενημέρωση και την αλλαγή των ρυθμίσεων οποιουδήποτε χρήστη, καθώς και πολλών ρυθμίσεων του συστήματος."
|
||||
"newUserHintSubFolder": "π.χ. '/subfolder' - κενό για προεπιλογή"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Ρυθμίσεις ιστότοπου",
|
||||
|
||||
@@ -550,7 +550,7 @@
|
||||
"download": "Download files"
|
||||
},
|
||||
"permissions-name": "Permissions",
|
||||
"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.",
|
||||
"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",
|
||||
"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
@@ -3,7 +3,7 @@
|
||||
"copyFile": "Copiar archivo",
|
||||
"copyToClipboard": "Copiar al portapapeles",
|
||||
"moveFile": "Mover archivo",
|
||||
"permalink": "Enlace permanente",
|
||||
"permalink": "Link sin expiración",
|
||||
"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 elemento"
|
||||
"goToItem": "Ir al artículo"
|
||||
},
|
||||
"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 encuentró lo que buscas, tal vez el enlace expiró.",
|
||||
"failedToConnectToServer": "No se ha podido conectar con el servidor, ¿Sigue funcionando?"
|
||||
"shareNotFound": "No se encuentra 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 el panel lateral",
|
||||
"b": "Mostrar/ocultar la barra 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. Para saber más, visita la",
|
||||
"slash": "Abrir panel de búsqueda",
|
||||
"description": "Aquí puedes ver todos los atajos de teclado básicos. Si quieres saber más, visita la",
|
||||
"slash": "Activar búsqueda",
|
||||
"wiki": "documentación oficial de FileBrowser Quantum",
|
||||
"marqueeSelect": "Selecciona archivos o carpetas con el mouse arrastrando en un espacio vacio"
|
||||
"marqueeSelect": "seleccionar varios archivos o directorios arrastrando sobre el espacio vacío"
|
||||
},
|
||||
"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 Archivos",
|
||||
"optionalPassword": "Contraseña: (opcional)",
|
||||
"upload": "Subir",
|
||||
"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": "Se detectaron conflictos",
|
||||
"conflictsDetected": "Conflictos detectados",
|
||||
"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": "Navegación rápida",
|
||||
"quickJump": "Salta rápidamente a otros archivos",
|
||||
"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 esta ventana, todos los archivos que estés subiendo se interrumpirán, y puede que algunos queden incompletos. ¿Que quieres hacer?",
|
||||
"closeWithActiveUploadsMessage": "Si cierras este aviso, todas las subidas activas se interrumpirán, y puede que algunos archivos 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": "Vista Previa",
|
||||
"hasPreview": "Tiene Vista Previa",
|
||||
"checksums": "Sumas de control",
|
||||
"hashAlgorithm": "Algoritmo hash",
|
||||
"hashAlgorithm": "Algoritmo Hash",
|
||||
"hashValue": "Valor Hash",
|
||||
"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",
|
||||
"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",
|
||||
"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 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`.",
|
||||
"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`.",
|
||||
"selectPath": "Seleccionar ruta",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Menor que:",
|
||||
"largerThan": "Mayor que:",
|
||||
"helpText1": "La busqueda se realiza por cada letra introducida (empezando de {minSearchLength})",
|
||||
"helpText1": "La busqueda se realiza por cada letra introducida (empezando de 3)",
|
||||
"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 el registro de nuevos usuarios",
|
||||
"allowSignup": "Permitir 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,16 +277,17 @@
|
||||
"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": "¡Recurso compartido eliminado!",
|
||||
"shareDeleted": "¡Compartido eliminado!",
|
||||
"singleClick": "Abrir con un solo clic",
|
||||
"userCommandsHelp": "Comandos permitidos (separados por espacios). Ejemplo:\n",
|
||||
"userCreated": "¡Usuario creado con éxito!",
|
||||
"userCreated": "¡Usuario creado!",
|
||||
"userDefaults": "Configuración de usuario por defecto",
|
||||
"userDeleted": "¡Usuario eliminado!",
|
||||
"userManagement": "Gestión de usuarios",
|
||||
@@ -298,8 +299,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": "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",
|
||||
"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",
|
||||
"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",
|
||||
@@ -322,46 +323,45 @@
|
||||
"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}.",
|
||||
"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."
|
||||
"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}."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Ajustes del sitio",
|
||||
"updateIsAvailable": "¡Actualización disponible!",
|
||||
"fileActions": "Acciones de archivo",
|
||||
"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",
|
||||
"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",
|
||||
"pickIcon": "Elija un icono",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "¡Enlace copiado!"
|
||||
@@ -374,16 +374,16 @@
|
||||
"unit": "Unidad"
|
||||
},
|
||||
"upload": {
|
||||
"abortUpload": "¿Cancelar subida de archivos?"
|
||||
"abortUpload": "¿Cancelar subida?"
|
||||
},
|
||||
"share": {
|
||||
"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",
|
||||
"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",
|
||||
"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 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",
|
||||
"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",
|
||||
"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 tema (claro u oscuro) para el recurso compartido, anulando las preferencias del usuario - Ocultará el botón de cambio de tema",
|
||||
"enforceDarkLightModeDescription": "Forzar un modo de tema específico (oscuro o claro) para el recurso compartido, anulando las preferencias del usuario.",
|
||||
"default": "Por defecto",
|
||||
"dark": "Oscuro",
|
||||
"light": "Claro",
|
||||
"enableOnlyOffice": "Activar el visor de OnlyOffice",
|
||||
"enableOnlyOfficeDescription": "Permitir la visualización de archivos Office usando OnlyOffice en este recurso compartido.",
|
||||
"enableOnlyOfficeDescription": "Permitir la visualización de archivos de oficina utilizando OnlyOffice en este recurso compartido.",
|
||||
"titleDefault": "{title}",
|
||||
"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:",
|
||||
"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",
|
||||
"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 subir archivos",
|
||||
"uploadShare": "Solo subidas",
|
||||
"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 podrá editar archivos en este recurso compartido.",
|
||||
"allowModifyDescription": "Cualquiera puede editar archivos en este recurso compartido.",
|
||||
"allowDelete": "Permitir la eliminación de archivos",
|
||||
"allowDeleteDescription": "Cualquiera podrá borrar archivos y carpetas de este recurso compartido.",
|
||||
"allowDeleteDescription": "Cualquiera puede borrar archivos y carpetas de este recurso compartido.",
|
||||
"allowReplacements": "Permitir la sustitución de archivos",
|
||||
"allowReplacementsDescription": "Cualquiera podrá reemplazar archivos existentes al subir/crear archivos con el mismo nombre.",
|
||||
"allowReplacementsDescription": "Cualquiera podrá reemplazar archivos existentes al subir archivos con el mismo nombre.",
|
||||
"allowCreate": "Permitir crear/subir archivos y carpetas",
|
||||
"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."
|
||||
"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."
|
||||
},
|
||||
"api": {
|
||||
"title": "Gestión de claves API",
|
||||
@@ -462,12 +462,12 @@
|
||||
"expires": "Expira el:",
|
||||
"actions": "Acciones",
|
||||
"enabled": "Habilitado",
|
||||
"disabled": "Desactivado",
|
||||
"permissionNote": "Debes de elegir al menos un permiso - Tu usuario también debe tenerlo.",
|
||||
"disabled": "Deshabilitado",
|
||||
"permissionNote": "Elije al menos un permiso - Tu usuario también debe tener ese permiso.",
|
||||
"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 borrada"
|
||||
"apiKeyDeleted": "Clave API eliminada"
|
||||
},
|
||||
"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 se encontró ninguna fuente. Añade una, o usa \"defaultEnabled: true\" en el archivo de configuración.",
|
||||
"noSources": "No hay fuentes disponibles. Añada una o active \"defaultEnabled: true\" en el archivo de configuración.",
|
||||
"unknown": "Desconocido",
|
||||
"simple": "Simple",
|
||||
"normal": "Normal",
|
||||
"complex": "Complejo",
|
||||
"highlyComplex": "Muy complejo",
|
||||
"highlyComplex": "Alta complejidad",
|
||||
"ready": "Listo",
|
||||
"indexing": "Escaneando",
|
||||
"indexing": "Indexación",
|
||||
"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 tú app de autenticación",
|
||||
"generate": "Escanear el código QR con la app de autenticación",
|
||||
"codeInputPlaceholder": "Código de 6 dígitos",
|
||||
"invalidCodeType": "Código inválido",
|
||||
"invalidCodeType": "Código inválido: debe ser de 6 dígitos",
|
||||
"enabledSuccessfully": "¡2FA activado!",
|
||||
"verificationFailed": "No se pudo autenticar",
|
||||
"verificationSucceed": "¡Verificación exitosa!",
|
||||
"generationFailed": "Hubo un error al generar el código"
|
||||
"verificationFailed": "Verificación fallida",
|
||||
"verificationSucceed": "Verificación exitosa",
|
||||
"generationFailed": "La generación del código falló"
|
||||
},
|
||||
"general": {
|
||||
"name": "Nombre",
|
||||
"path": "Ruta:",
|
||||
"path": "Ruta",
|
||||
"user": "Usuario",
|
||||
"group": "Grupo",
|
||||
"signup": "Crear cuenta",
|
||||
"signup": "Regístrarse",
|
||||
"password": "Contraseña",
|
||||
"username": "Nombre de usuario",
|
||||
"files": "Archivos",
|
||||
@@ -517,32 +517,32 @@
|
||||
"source": "Fuente{suffix}",
|
||||
"percentage": "{percentage}%",
|
||||
"type": "Tipo{suffix}",
|
||||
"size": "Tamaño{suffix}",
|
||||
"size": "Talla{suffix}",
|
||||
"body": "Cuerpo{suffix}",
|
||||
"home": "Inicio{suffix}",
|
||||
"clear": "Limpiar{suffix}",
|
||||
"clear": "Claro{suffix}",
|
||||
"help": "Ayuda{suffix}",
|
||||
"login": "Iniciar sesión{suffix}",
|
||||
"logout": "Cerrar sesión{suffix}",
|
||||
"login": "Inicio de sesión{suffix}",
|
||||
"logout": "Cierre de sesión{suffix}",
|
||||
"preview": "Vista previa{suffix}",
|
||||
"settings": "Ajustes{suffix}",
|
||||
"sources": "Fuentes{suffix}",
|
||||
"links": "Accesos directos{suffix}",
|
||||
"links": "Enlaces{suffix}",
|
||||
"tool": "Herramienta{suffix}",
|
||||
"used": "usado{suffix}",
|
||||
"status": "Estado{suffix}",
|
||||
"analyze": "Analizar{suffix}",
|
||||
"analyze": "Analice{suffix}",
|
||||
"results": "Resultados{suffix}",
|
||||
"users": "Usuarios{suffix}",
|
||||
"photos": "Fotos",
|
||||
"audio": "Audio{suffix}",
|
||||
"videos": "Vídeos{suffix}",
|
||||
"documents": "Documentos{suffix}",
|
||||
"archives": "Archivos comprimidos{suffix}",
|
||||
"archives": "Archivos{suffix}",
|
||||
"images": "Imágenes{suffix}",
|
||||
"music": "Música{suffix}",
|
||||
"pdf": "PDF{suffix}",
|
||||
"search": "Buscar{suffix}",
|
||||
"search": "Buscar en{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": "Claro{suffix}",
|
||||
"light": "Luz{suffix}",
|
||||
"title": "Tema{suffix}",
|
||||
"expires": "Expira en{suffix}",
|
||||
"scope": "Alcance{suffix}",
|
||||
"rules": "Reglas{suffix}",
|
||||
"downloads": "Descargas{suffix}",
|
||||
"submit": "Aceptar{suffix}",
|
||||
"submit": "Inicio de sesión{suffix}",
|
||||
"navigation": "Navegación{suffix}",
|
||||
"metadata": "Metadata{suffix}",
|
||||
"loading": "Cargando...{suffix}",
|
||||
"metadata": "Metadatos{suffix}",
|
||||
"loading": "Cargando{suffix}",
|
||||
"add": "Añadir{suffix}",
|
||||
"cancel": "Cancelar{suffix}",
|
||||
"verify": "Verificar{suffix}",
|
||||
"verify": "Verifique{suffix}",
|
||||
"close": "Cerrar{suffix}",
|
||||
"copy": "Copiar{suffix}",
|
||||
"create": "Crear{suffix}",
|
||||
"copy": "Copia{suffix}",
|
||||
"create": "Cree{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": "Publicar{suffix}",
|
||||
"rename": "Cambiar nombre{suffix}",
|
||||
"replace": "Reemplazar{suffix}",
|
||||
"publish": "Publique{suffix}",
|
||||
"rename": "Cambie el nombre de{suffix}",
|
||||
"replace": "Sustituir{suffix}",
|
||||
"save": "Guardar{suffix}",
|
||||
"schedule": "Horario{suffix}",
|
||||
"select": "Seleccionar{suffix}",
|
||||
"select": "Seleccione{suffix}",
|
||||
"share": "Compartir{suffix}",
|
||||
"update": "Actualizar{suffix}",
|
||||
"upload": "Subir Archivos{suffix}",
|
||||
"openFile": "Abrir Archivo{suffix}",
|
||||
"continue": "Continuar{suffix}",
|
||||
"update": "Actualización{suffix}",
|
||||
"upload": "Cargar{suffix}",
|
||||
"openFile": "Abrir expediente{suffix}",
|
||||
"continue": "Continúe en{suffix}",
|
||||
"edit": "Editar{suffix}",
|
||||
"pause": "Pausar{suffix}",
|
||||
"resume": "Reanudar{suffix}",
|
||||
"pause": "Pausa{suffix}",
|
||||
"resume": "Currículum{suffix}",
|
||||
"retry": "Reintentar{suffix}",
|
||||
"disable": "Desactivar{suffix}",
|
||||
"confirm": "Confirmar{suffix}",
|
||||
"stay": "Quedarme{suffix}",
|
||||
"scopes": "Alcances{suffix}",
|
||||
"confirm": "Confirme{suffix}",
|
||||
"stay": "Permanezca en{suffix}",
|
||||
"scopes": "Visores{suffix}",
|
||||
"commands": "Comandos{suffix}",
|
||||
"uploading": "Subiendo{suffix}",
|
||||
"uploading": "Cargar{suffix}",
|
||||
"paused": "En pausa{suffix}",
|
||||
"conflict": "Conflicto{suffix}",
|
||||
"change": "Cambiar{suffix}"
|
||||
"change": "Cambia{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Reglas de acceso",
|
||||
"deny": "Bloquear",
|
||||
"deniedUsers": "Usuarios bloqueados",
|
||||
"deniedGroups": "Grupos bloqueados",
|
||||
"rules": "Normas de acceso",
|
||||
"deny": "Denegar",
|
||||
"deniedUsers": "Usuarios denegados",
|
||||
"deniedGroups": "Grupos denegados",
|
||||
"allowedUsers": "Usuarios permitidos",
|
||||
"allowedGroups": "Grupos permitidos",
|
||||
"allow": "Permitir",
|
||||
"allowDeny": "Permitir/Bloquear",
|
||||
"allowDeny": "Permitir/Denegar",
|
||||
"userGroup": "Usuario/Grupo",
|
||||
"enterName": "Escribe el nombre del usuario o grupo",
|
||||
"enterName": "Escribe el nombre del usuario, o del grupo",
|
||||
"deleted": "Regla de acceso eliminada",
|
||||
"added": "Regla de acceso añadida",
|
||||
"accessManagement": "Gestión de accesos",
|
||||
"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"
|
||||
"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."
|
||||
},
|
||||
"fileLoading": {
|
||||
"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",
|
||||
"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",
|
||||
"uploadChunkSizeMb": "Velocidad de subida (en MB)",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"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 para los archivos de Office",
|
||||
"previewOfficeDescription": "Mostrar miniaturas de los tipos de archivos de Office en el tipo de vista: Lista",
|
||||
"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 mouse por encima de los archivos que tengan una miniatura",
|
||||
"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",
|
||||
"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 desactivar",
|
||||
"disableFileExtensions": "Escribe aquí las extensiones para deshabilitar",
|
||||
"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.",
|
||||
"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!",
|
||||
"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!",
|
||||
"errorSavingFile": "Error al guardar el archivo: {error}"
|
||||
},
|
||||
"player": {
|
||||
@@ -727,20 +727,20 @@
|
||||
"emptyQueue": "La cola de reproducción está vacia..."
|
||||
},
|
||||
"fileSizeAnalyzer": {
|
||||
"title": "Analizador de archivos",
|
||||
"title": "Analizador de tamaño de archivos",
|
||||
"searchLocation": "Buscar ubicación",
|
||||
"selectPath": "Selecciona el lugar de búsqueda",
|
||||
"largerThan": "Mayor que (en MB)",
|
||||
"selectPath": "Seleccione el lugar de búsqueda",
|
||||
"largerThan": "Mayor que (MB)",
|
||||
"includeFolders": "Incluir carpetas",
|
||||
"includeFoldersDescription": "Incluir carpetas en el análisis",
|
||||
"includeFoldersDescription": "Incluir directorios en el análisis",
|
||||
"totalFiles": "Archivos totales{suffix}",
|
||||
"totalSize": "Tamaño total{suffix}",
|
||||
"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",
|
||||
"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.",
|
||||
"relativePercentage": "Porcentaje relativo{suffix}",
|
||||
"percentageOfResults": "{percentage}% de los resultados"
|
||||
"percentageOfResults": "{percentage}% de resultados"
|
||||
},
|
||||
"fileTypes": {
|
||||
"video": "Vídeo",
|
||||
@@ -753,65 +753,65 @@
|
||||
"other": "Otros"
|
||||
},
|
||||
"tools": {
|
||||
"description": "Utilidades y herramientas que te seŕan de ayuda",
|
||||
"description": "Utilidades y herramientas de análisis para sus archivos",
|
||||
"sizeAnalyzer": {
|
||||
"name": "Analizador de archivos",
|
||||
"description": "Encuentra que archivos y carpetas están ocupando más espacio en tu disco."
|
||||
"name": "Analizador de tamaño de archivos",
|
||||
"description": "Visualice archivos y directorios de gran tamaño con un mapa de árbol interactivo"
|
||||
},
|
||||
"title": "Herramientas",
|
||||
"duplicateFinder": {
|
||||
"name": "Buscador de archivos duplicados",
|
||||
"description": "Encuentra y gestiona archivos duplicados para recuperar espacio."
|
||||
"description": "Encuentre y gestione los archivos duplicados para recuperar espacio de almacenamiento"
|
||||
},
|
||||
"materialIconPicker": {
|
||||
"name": "Material Icons",
|
||||
"description": "¡Explora y descubre Iconos de la colección Material Icons!",
|
||||
"name": "Icono de selección de material",
|
||||
"description": "Navega y busca Material Icons con vista previa en directo",
|
||||
"searchPlaceholder": "Buscar iconos...",
|
||||
"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",
|
||||
"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",
|
||||
"useCustomIcon": "Utilizar icono personalizado: {name}",
|
||||
"iconLibraryUrl": "fonts.google.com/icons",
|
||||
"browseFullLibrary": "Mira la la colección completa aqui"
|
||||
"browseFullLibrary": "Explorar la biblioteca completa de iconos"
|
||||
},
|
||||
"toolNotFound": "No se encontró la herramienta",
|
||||
"toolNotFound": "Herramienta no encontrada",
|
||||
"backToTools": "Volver a Herramientas"
|
||||
},
|
||||
"messages": {
|
||||
"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",
|
||||
"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.",
|
||||
"reassignPath": "Reasignar ruta",
|
||||
"pathReassigned": "¡La ruta se actualizó correctamente!",
|
||||
"pathReassignFailed": "No se ha podido actualizar la ruta. Inténtalo de nuevo"
|
||||
"pathReassigned": "¡Ruta actualizada correctamente!",
|
||||
"pathReassignFailed": "No se ha podido actualizar la ruta. Inténtelo de nuevo."
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "Buscador de archivos duplicados",
|
||||
"minSize": "Tamaño mínimo (en MB){suffix}",
|
||||
"minSizeHint": "Solo analizar archivos que sean mayores que ese tamaño",
|
||||
"minSize": "Tamaño mínimo (MB){suffix}",
|
||||
"minSizeHint": "Analizar sólo archivos mayores que este tamaño (en megabytes)",
|
||||
"useChecksums": "Comparar el contenido de los archivos",
|
||||
"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",
|
||||
"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.",
|
||||
"findDuplicates": "Buscar duplicados",
|
||||
"groupsFound": "Grupos de duplicados{suffix}",
|
||||
"totalWastedSpace": "Espacio desperdiciado{suffix}",
|
||||
"groupsFound": "Grupos duplicados encontrados{suffix}",
|
||||
"totalWastedSpace": "Espacio total desperdiciado{suffix}",
|
||||
"wasted": "Desperdiciado{suffix}",
|
||||
"emptyState": "Introduce los criterios de búsqueda y haz clic en \"Buscar duplicados\" para encontrar que archivos pueden estar gastando espacio."
|
||||
"emptyState": "Introduzca los criterios de búsqueda y haga clic en Buscar duplicados para identificar los archivos duplicados."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notificaciones",
|
||||
"description": "Aquí verás todas las notificaciones de tu sesión actual",
|
||||
"empty": "Aún no tienes notificaciones",
|
||||
"description": "Ver todas las notificaciones de la sesión actual",
|
||||
"empty": "Aún no hay notificaciones",
|
||||
"clearAll": "Borrar todo",
|
||||
"clearConfirm": "¿Estás seguro de que quieres borrar todo el historial de notificaciones?",
|
||||
"total": "{count} notificaciones"
|
||||
},
|
||||
"events": {
|
||||
"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..."
|
||||
"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..."
|
||||
}
|
||||
}
|
||||
@@ -273,6 +273,7 @@
|
||||
"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",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "par exemple \"/sous-dossier\" - vide par défaut"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Paramètres du site",
|
||||
|
||||
@@ -274,6 +274,7 @@
|
||||
"download": "הורדת קבצים"
|
||||
},
|
||||
"permissions-name": "הרשאות",
|
||||
"permissionsHelp": "אתה יכול להגדיר את המשתמש להיות מנהל מערכת או לבחור את ההרשאות בנפרד. אם תבחר \"מנהל מערכת\", כל ההרשאות ייבחרו אוטומטית. ניהול המשתמשים נשאר הרשאה של מנהל מערכת.\n",
|
||||
"profileSettings": "הגדרות פרופיל",
|
||||
"ruleExample1": "מנע גישה לקבצים נסתרים (כל קובץ שמתחיל בנקודה, לדוגמא .git)",
|
||||
"ruleExample2": "חסימת גישה לקובץ בשם Caddyfile בתחום הראשי.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "פרוקסי"
|
||||
},
|
||||
"newUserHintSubFolder": "לדוגמה, '/subfolder' - ריק עבור ברירת המחדל",
|
||||
"permissionsHelp": "הרשאות המשתמש שולטות בגישה לסוגי הפעולות שהמשתמש יכול לבצע. גישה לניהול מעניקה שליטה מלאה לעדכון ולשינוי הגדרות המשתמש, כמו גם הגדרות רבות של המערכת."
|
||||
"newUserHintSubFolder": "לדוגמה, '/subfolder' - ריק עבור ברירת המחדל"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "הגדרות",
|
||||
|
||||
@@ -274,6 +274,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "pl. '/subfolder' - üres az alapértelmezéshez"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Oldalbeállítások",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "es. '/sottocartella' - vuoto per l'impostazione predefinita"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Impostazioni del sito",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"download": "ファイルのダウンロード"
|
||||
},
|
||||
"permissions-name": "権限",
|
||||
"permissionsHelp": "ユーザーを管理者に設定するか、権限を個別に設定できます。「管理者」を選択した場合、その他のすべての選択肢は自動的に設定されます。ユーザーの管理は管理者の権限として保持されます。",
|
||||
"profileSettings": "プロファイル設定",
|
||||
"ruleExample1": "各フォルダ内で名前がドットで始まるファイル(例:.git、.gitignore)へのアクセスを制限します。",
|
||||
"ruleExample2": "スコープのルートパスで名前がCaddyfileのファイルへのアクセスを制限します。",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "プロキシ"
|
||||
},
|
||||
"newUserHintSubFolder": "例 '/subfolder' - デフォルトは空白",
|
||||
"permissionsHelp": "ユーザー権限は、ユーザーが実行できるアクションの種類へのアクセスを制御します。管理者アクセスは、多くのシステム設定だけでなく、任意のユーザーの設定を更新および変更するための完全なコントロールを提供します。"
|
||||
"newUserHintSubFolder": "例 '/subfolder' - デフォルトは空白"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "サイト設定",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"download": "파일 다운로드"
|
||||
},
|
||||
"permissions-name": "권한",
|
||||
"permissionsHelp": "사용자를 관리자로 만들거나 권한을 부여할 수 있습니다. 관리자를 선택하면, 모든 옵션이 자동으로 선택됩니다. 사용자 관리는 현재 관리자만 할 수 있습니다.\n",
|
||||
"profileSettings": "프로필 설정",
|
||||
"ruleExample1": "점(.)으로 시작하는 모든 파일의 접근을 방지합니다.(예 .git, .gitignore)\n",
|
||||
"ruleExample2": "Caddyfile파일의 접근을 방지합니다.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "프록시"
|
||||
},
|
||||
"newUserHintSubFolder": "예: '/subfolder' - 기본값은 비어 있습니다.",
|
||||
"permissionsHelp": "사용자 권한은 사용자가 수행할 수 있는 작업 유형에 대한 액세스를 제어합니다. 관리자 권한은 모든 사용자 설정은 물론 여러 시스템 설정을 업데이트하고 변경할 수 있는 모든 권한을 부여합니다."
|
||||
"newUserHintSubFolder": "예: '/subfolder' - 기본값은 비어 있습니다."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "사이트 설정",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "bijv. '/submap' - leeg voor standaard"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Site-instellingen",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Pełnomocnik"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "np. '/subfolder' - domyślnie puste"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Ustawienia Strony",
|
||||
|
||||
@@ -275,6 +275,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "Por exemplo, '/subfolder' - em branco para o padrão"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Configurações do site",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "por exemplo, '/subpasta' - em branco por defeito"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Configurações do site",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "de exemplu \"/subfolder\" - gol pentru implicit"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Setări site",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"download": "Скачать файлы"
|
||||
},
|
||||
"permissions-name": "Права доступа",
|
||||
"permissionsHelp": "Можно настроить пользователя как администратора или выбрать разрешения индивидуально. При выборе \"Администратор\", все остальные параметры будут автоматически выбраны. Управление пользователями - привилегия администратора.\n",
|
||||
"profileSettings": "Настройки профиля",
|
||||
"ruleExample1": "Запретить доступ к любому скрытому файлу (например: .git, .gitignore) в каждой папке.\n",
|
||||
"ruleExample2": "блокирует доступ к файлу с именем Caddyfile в корневой области.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Прокси-сервер"
|
||||
},
|
||||
"newUserHintSubFolder": "например, '/subfolder' - пустое место по умолчанию",
|
||||
"permissionsHelp": "Разрешения пользователя контролируют, какие действия он может выполнять. Доступ администратора дает полный контроль над обновлением и изменением настроек любого пользователя, а также многих системных настроек."
|
||||
"newUserHintSubFolder": "например, '/subfolder' - пустое место по умолчанию"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Настройки сайта",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "napr. '/subfolder' - prázdne pre predvolené nastavenie"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Nastavenia stránky",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "t.ex. \"/undermapp\" - tom för standard"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "System inställningar",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"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.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"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."
|
||||
"newUserHintSubFolder": "örneğin '/subfolder' - varsayılan için boş"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Site ayarları!",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"download": "Завантажити файли"
|
||||
},
|
||||
"permissions-name": "Дозволи",
|
||||
"permissionsHelp": "Можна настроїти користувача як адміністратора або вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n",
|
||||
"profileSettings": "Налаштування профілю",
|
||||
"ruleExample1": "запобігти доступу до будь-якого прихованого файлу (наприклад: .git, .gitignore) у кожній папці.\n",
|
||||
"ruleExample2": "блокує доступ до файлу з ім'ям Caddyfile у кореневій області.",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Довірена особа"
|
||||
},
|
||||
"newUserHintSubFolder": "наприклад, '/subfolder' - порожній за замовчуванням",
|
||||
"permissionsHelp": "Дозволи користувача контролюють доступ до того, які дії може виконувати користувач. Доступ адміністратора дає повний контроль над оновленням і зміною будь-яких налаштувань користувача, а також багатьох системних налаштувань."
|
||||
"newUserHintSubFolder": "наприклад, '/subfolder' - порожній за замовчуванням"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Налаштування сайту",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"download": "下载文件"
|
||||
},
|
||||
"permissions-name": "权限",
|
||||
"permissionsHelp": "您可以将该用户设置为管理员或单独选择各项权限。如果您选择了“管理员”,则其他的选项会被自动选中,同时该用户可以管理其他用户。\n",
|
||||
"profileSettings": "个人设置",
|
||||
"ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore)。\n",
|
||||
"ruleExample2": "阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "代理"
|
||||
},
|
||||
"newUserHintSubFolder": "例如\"/子文件夹\"--默认为空",
|
||||
"permissionsHelp": "用户权限控制用户可以执行哪些类型的操作。管理员权限可完全控制更新和更改任何用户的设置以及许多系统设置。"
|
||||
"newUserHintSubFolder": "例如\"/子文件夹\"--默认为空"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "网站设置",
|
||||
@@ -660,7 +660,7 @@
|
||||
"publish": "出版{suffix}",
|
||||
"rename": "重新命名{suffix}",
|
||||
"replace": "更换{suffix}",
|
||||
"save": "保存{suffix}",
|
||||
"save": "节省{suffix}",
|
||||
"schedule": "时间安排{suffix}",
|
||||
"select": "选择{suffix}",
|
||||
"share": "分享{suffix}",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"download": "下載檔案"
|
||||
},
|
||||
"permissions-name": "權限",
|
||||
"permissionsHelp": "您可以將該使用者設置為管理員,也可以單獨選擇各項權限。如果選擇了「管理員」,則其他的選項會被自動勾上,同時該使用者可以管理其他使用者。",
|
||||
"profileSettings": "個人設定",
|
||||
"ruleExample1": "封鎖使用者存取所有資料夾下任何以 . 開頭的檔案(隱藏文件, 例如: .git, .gitignore)。",
|
||||
"ruleExample2": "封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。",
|
||||
@@ -322,8 +323,7 @@
|
||||
"oidc": "OIDC",
|
||||
"proxy": "代理"
|
||||
},
|
||||
"newUserHintSubFolder": "例如 '/subfolder' - 預設為空白",
|
||||
"permissionsHelp": "使用者權限可控制使用者可執行的動作類型。管理員存取權可完全控制更新和變更任何使用者的設定以及許多系統設定。"
|
||||
"newUserHintSubFolder": "例如 '/subfolder' - 預設為空白"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "網站設定",
|
||||
|
||||
@@ -421,8 +421,9 @@ export const getters = {
|
||||
},
|
||||
officeViewingDisabled: filename => {
|
||||
const ext = ' ' + getFileExtension(filename)
|
||||
const disabledList = state.user.disableOnlyOfficeExt || ''
|
||||
if (disabledList !== '') {
|
||||
const hasDisabled = state.shareInfo?.disableOfficePreviewExt || state.user.disableOfficePreviewExt
|
||||
if (hasDisabled) {
|
||||
const disabledList = state.shareInfo?.disableOfficePreviewExt + ' ' + state.user.disableOfficePreviewExt
|
||||
const disabledExts = ' ' + disabledList.toLowerCase()
|
||||
if (disabledExts.includes(ext.toLowerCase())) {
|
||||
return true
|
||||
|
||||
@@ -44,10 +44,9 @@ export default function downloadFiles(items) {
|
||||
|
||||
async function startDownload(config, files, hash = "") {
|
||||
try {
|
||||
await filesApi.download(config, files, hash);
|
||||
filesApi.download(config, files, hash);
|
||||
notify.showSuccessToast("Downloading...");
|
||||
} catch (e) {
|
||||
console.error("Download failed:", e);
|
||||
notify.showError(`Error downloading: ${e.message || e}`);
|
||||
notify.showError(`Error downloading: ${e}`);
|
||||
}
|
||||
}
|
||||
@@ -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,13 +25,3 @@ 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;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ export default {
|
||||
removePrefix,
|
||||
getApiPath,
|
||||
extractSourceFromPath,
|
||||
fixDownloadURL,
|
||||
base64Encode,
|
||||
joinPath,
|
||||
goToItem,
|
||||
@@ -192,3 +193,29 @@ 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}`;
|
||||
}
|
||||
@@ -308,7 +308,7 @@ export default {
|
||||
showLineNumbers: true,
|
||||
theme: this.isDarkMode ? "ace/theme/tomorrow_night_bright" : "ace/theme/github",
|
||||
readOnly: this.editorReadOnly,
|
||||
wrap: state?.wrapEditor || false,
|
||||
wrap: false,
|
||||
enableMobileMenu: !this.viewerMode,
|
||||
useWorker: false,
|
||||
scrollPastEnd: 0.5,
|
||||
|
||||
@@ -471,7 +471,7 @@ export default {
|
||||
styles['--list-icon-image-size'] = `${iconImageSize}em`;
|
||||
} else {
|
||||
// Normal view
|
||||
styles['--item-width'] = `calc(${(100 / this.numColumns)}% - 1em)`;
|
||||
styles['--item-width'] = `calc(${(100 / this.numColumns).toFixed(2)}% - 1em)`;
|
||||
styles['--item-height'] = 'auto';
|
||||
}
|
||||
|
||||
@@ -956,7 +956,6 @@ export default {
|
||||
console.error("Error moving/copying items:", error);
|
||||
} finally {
|
||||
mutations.setLoading("listing", false);
|
||||
mutations.setReload(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -88,9 +88,9 @@
|
||||
</i>
|
||||
</div>
|
||||
<div class="form-flex-group">
|
||||
<input class="input form-form flat-right disable-viewing"
|
||||
<input class="input form-form flat-right"
|
||||
:class="{ 'form-invalid': !validateExtensions(formDisablePreviews) }" type="text"
|
||||
:placeholder="$t('profileSettings.disableFileExtensions')"
|
||||
:placeholder="$t('profileSettings.disableFileExtensions')" id="disablePreviews"
|
||||
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 disable-viewing"
|
||||
<input class="input form-form flat-right"
|
||||
:class="{ 'form-invalid': !validateExtensions(formDisabledViewing) }" type="text"
|
||||
:placeholder="$t('profileSettings.disableFileExtensions')"
|
||||
:placeholder="$t('profileSettings.disableFileExtensions')" id="disableViewing"
|
||||
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(formDisableOfficeViewing) }" type="text"
|
||||
:placeholder="$t('profileSettings.disableFileExtensions')" id="disableOfficeViewing"
|
||||
v-model="formDisableOfficeViewing" />
|
||||
<button type="button" class="button form-button flat-left" @click="submitDisableOfficeViewingChange">
|
||||
: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">
|
||||
{{ $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
|
||||
formDisableOfficeViewing: "", // holds temporary input before saving
|
||||
formDisableOfficePreview: "", // 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.formDisableOfficeViewing = this.localuser.disableOnlyOfficeExt;
|
||||
this.formDisableOfficePreview = this.localuser.disableOfficePreviewExt;
|
||||
},
|
||||
methods: {
|
||||
showTooltip(event, text) {
|
||||
@@ -279,12 +279,12 @@ export default {
|
||||
this.localuser.disableViewingExt = this.formDisabledViewing;
|
||||
this.updateSettings();
|
||||
},
|
||||
submitDisableOfficeViewingChange() {
|
||||
if (!this.validateExtensions(this.formDisableOfficeViewing)) {
|
||||
submitDisableOfficePreviewChange() {
|
||||
if (!this.validateExtensions(this.formDisableOfficePreview)) {
|
||||
notify.showError("Invalid input, does not match requirement.");
|
||||
return;
|
||||
}
|
||||
this.localuser.disableOnlyOfficeExt = this.formDisableOfficeViewing;
|
||||
this.localuser.disableOfficePreviewExt = this.formDisableOfficePreview;
|
||||
this.updateSettings();
|
||||
},
|
||||
setColor(string) {
|
||||
@@ -317,7 +317,7 @@ export default {
|
||||
"quickDownload",
|
||||
"disablePreviewExt",
|
||||
"disableViewingExt",
|
||||
"disableOnlyOfficeExt",
|
||||
"disableOfficePreviewExt",
|
||||
"deleteWithoutConfirming",
|
||||
"preview",
|
||||
"disableQuickToggles",
|
||||
@@ -358,7 +358,9 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.disable-viewing {
|
||||
#disablePreviews,
|
||||
#disableViewing,
|
||||
#disableOfficePreview {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button :disabled="item.shareType == 'upload'" class="action copy-clipboard" :data-clipboard-text="item.downloadURL" v-if="item.downloadURL"
|
||||
<button :disabled="item.shareType == 'upload'" class="action copy-clipboard" :data-clipboard-text="fixDownloadURL(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,12 +74,14 @@
|
||||
|
||||
<script>
|
||||
import { notify } from "@/notify";
|
||||
import { shareApi } from "@/api";
|
||||
import { publicApi, 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",
|
||||
@@ -198,10 +200,19 @@ export default {
|
||||
return fromNow(time);
|
||||
},
|
||||
/**
|
||||
* @param {any} share
|
||||
* @param {any} item
|
||||
*/
|
||||
buildLink(share) {
|
||||
return share.shareURL;
|
||||
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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
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,7 +64,6 @@ 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' });
|
||||
|
||||
Reference in new issue
Block a user