mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-04 16:50:08 -05:00
Compare commits
26
Commits
main
...
v1.3.1-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d6e429a38 | ||
|
|
5a7891c833 | ||
|
|
27e62213c3 | ||
|
|
b7c020f8a4 | ||
|
|
b208e6396a | ||
|
|
955a7de0d6 | ||
|
|
17e2fce60a | ||
|
|
9b8e9c6aef | ||
|
|
be244cf0ee | ||
|
|
cf196111e5 | ||
|
|
c9dd63f326 | ||
|
|
5344a836f4 | ||
|
|
62a6be2852 | ||
|
|
b18423a496 | ||
|
|
614e5bb826 | ||
|
|
5c4a4d76da | ||
|
|
46a884d07a | ||
|
|
e28f98f355 | ||
|
|
2af75311bd | ||
|
|
82c0182c22 | ||
|
|
c8f0b5abea | ||
|
|
d3d029d1e9 | ||
|
|
f39002f375 | ||
|
|
a658f100c0 | ||
|
|
c2d255a25e | ||
|
|
b0be26b99e |
No files matched your search
@@ -2,6 +2,28 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v1.3.1-beta
|
||||
|
||||
**Security**:
|
||||
- Patched Stored XSS in public share page via unsanitized share metadata (text/template misuse) GHSA-r633-fcgp-m532
|
||||
- Patched Incomplete Remediation of CVE-2026-27611: Password-Protected Share Bypass via /public/api/share/info GHSA-525j-95gf-766f
|
||||
|
||||
**New Features**:
|
||||
- More user options for settings (#2072) (#2067):
|
||||
- Option to disable thumbnails `userDefaults.preview.audio` and `userDefaults.preview.models` for Audio and 3D Models.
|
||||
- Option to disable files in the Tree navigation `userDefaults.hideFilesInTree`
|
||||
- Option to disable source files deletion when creating/extracting archives. `userDefaults.deleteAfterArchive`
|
||||
|
||||
**Notes**:
|
||||
- Share icon does not show in share listing or for shares for other users.
|
||||
- File Size Analyzer tool max items increased from 100 to 200.
|
||||
- changed symlink detection logic.
|
||||
|
||||
**BugFixes**:
|
||||
- 405 response code error on Webdav in 1.3.0-beta (#2054)
|
||||
- Motion Preview setting not saving when changed in profile settings.
|
||||
- Context menu on tools not working.
|
||||
|
||||
## v1.3.0-beta
|
||||
|
||||
Note: a potentially breaking change for docker users: \the default user is now "filebrowser" 1000:1000 instead of root.
|
||||
@@ -59,6 +81,22 @@ Note: a potentially breaking change for docker users: \the default user is now "
|
||||
**BugFixes**:
|
||||
- Long folder names get cut off at top navigation bar (#1934)
|
||||
|
||||
## v1.2.2-stable
|
||||
|
||||
**Security**:
|
||||
- Patched Stored XSS in public share page via unsanitized share metadata (text/template misuse)
|
||||
|
||||
**New Features**:
|
||||
- "Divider" option in sidebar links to add a text or divider between links ()
|
||||
- shares offer a "go to source Location" sidebar link and button when editing a share.
|
||||
|
||||
**Notes**:
|
||||
- Share edit/delete permissions are scoped to the user's shares rather than global (#2050)
|
||||
|
||||
**BugFixes**:
|
||||
- fixed the requirement that the database path needed to be set in the config file, now it loads `FILEBROWSER_DATABASE` value by default, fallback to config file property.
|
||||
- Error downloading zipped directory: no such file or directory (users with scope) #2015
|
||||
|
||||
## v1.2.1-stable
|
||||
|
||||
**BugFixes**:
|
||||
|
||||
@@ -14,6 +14,9 @@ COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css" ]
|
||||
RUN mkdir -p ./http/dist
|
||||
COPY [ "./backend/http/embed/", "./http/dist/"]
|
||||
|
||||
# generate duplicate 1.1MB files
|
||||
RUN dd if=/dev/zero of=/app/frontend/tests/playwright-files/1.1MB.bin bs=1024 count=1126 && dd if=/dev/zero of=/app/frontend/tests/playwright-files/1.1mb.bin bs=1024 count=1126
|
||||
|
||||
# Run tests
|
||||
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
|
||||
RUN ./filebrowser set -u testuser1,testuser1
|
||||
|
||||
@@ -8,8 +8,10 @@ userDefaults:
|
||||
image: true
|
||||
popup: true
|
||||
video: true
|
||||
audio: true
|
||||
models: true
|
||||
office: true
|
||||
highQuality: true
|
||||
permissions:
|
||||
download: true
|
||||
create: true
|
||||
create: true
|
||||
@@ -26,8 +26,10 @@ userDefaults:
|
||||
image: true
|
||||
popup: true
|
||||
video: true
|
||||
audio: true
|
||||
office: true
|
||||
models: true
|
||||
highQuality: true
|
||||
permissions:
|
||||
download: true
|
||||
create: true
|
||||
create: true
|
||||
@@ -11,14 +11,14 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
*/
|
||||
export default defineConfig({
|
||||
globalSetup: "./tests/playwright/proxy-setup.ts",
|
||||
timeout: 5000,
|
||||
timeout: 10000,
|
||||
testDir: "./tests/playwright/proxy",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: false,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: false,
|
||||
/* Retry on CI only */
|
||||
retries: 2,
|
||||
retries: 3,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 1, // required for now! todo parallel some tests
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
|
||||
@@ -25,6 +25,9 @@ import (
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
// CheckPermissionsFunc allows tests to override CheckPermissions behavior
|
||||
var CheckPermissionsFunc = checkPermissionsImpl
|
||||
|
||||
// processDirectoryMetadata extracts metadata for audio/video files in directories
|
||||
func processDirectoryMetadata(response *iteminfo.ExtendedFileInfo, idx *indexing.Index, opts utils.FileOptions) {
|
||||
metadataCount := 0
|
||||
@@ -116,7 +119,13 @@ func finalizeResponse(response *iteminfo.ExtendedFileInfo, info *iteminfo.FileIn
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPermissions validates user access and returns the resolved index path and user scope
|
||||
func CheckPermissions(opts utils.FileOptions, access *access.Storage, user *users.User) (string, string, error) {
|
||||
return CheckPermissionsFunc(opts, access, user)
|
||||
}
|
||||
|
||||
// checkPermissionsImpl is the actual implementation of CheckPermissions
|
||||
func checkPermissionsImpl(opts utils.FileOptions, access *access.Storage, user *users.User) (string, string, error) {
|
||||
if access == nil {
|
||||
return "", "", fmt.Errorf("access not provided")
|
||||
}
|
||||
@@ -203,7 +212,15 @@ func GetDirItems(opts utils.FileOptions, access *access.Storage, user *users.Use
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// FileInfoFasterFunc is a variable that can be mocked in tests
|
||||
var FileInfoFasterFunc = fileInfoFasterImpl
|
||||
|
||||
func FileInfoFaster(opts utils.FileOptions, access *access.Storage, user *users.User, share *share.Storage) (*iteminfo.ExtendedFileInfo, error) {
|
||||
return FileInfoFasterFunc(opts, access, user, share)
|
||||
}
|
||||
|
||||
// fileInfoFasterImpl is the actual implementation of FileInfoFaster
|
||||
func fileInfoFasterImpl(opts utils.FileOptions, access *access.Storage, user *users.User, share *share.Storage) (*iteminfo.ExtendedFileInfo, error) {
|
||||
response := &iteminfo.ExtendedFileInfo{}
|
||||
indexPath, userScope, err := CheckPermissions(opts, access, user)
|
||||
if err != nil {
|
||||
@@ -239,21 +256,20 @@ func FileInfoFaster(opts utils.FileOptions, access *access.Storage, user *users.
|
||||
return response, err
|
||||
}
|
||||
}
|
||||
|
||||
defer finalizeResponse(response, info, response.RealPath, user, userScope)
|
||||
if opts.SkipExtendedAttrs {
|
||||
return response, nil
|
||||
}
|
||||
if share != nil && user.Permissions.Share {
|
||||
if share != nil && user.Permissions.Share && opts.ShowSharedAttr {
|
||||
for i := range response.Files {
|
||||
file := &response.Files[i]
|
||||
file.IsShared = share.IsShared(response.Path+file.Name, idx.Path)
|
||||
file.IsShared = share.IsShared(response.Path+file.Name, idx.Path, user.ID)
|
||||
}
|
||||
for i := range response.Folders {
|
||||
folder := &response.Folders[i]
|
||||
folder.IsShared = share.IsShared(response.Path+folder.Name, idx.Path)
|
||||
folder.IsShared = share.IsShared(response.Path+folder.Name, idx.Path, user.ID)
|
||||
}
|
||||
response.IsShared = share.IsShared(response.Path, idx.Path)
|
||||
response.IsShared = share.IsShared(response.Path, idx.Path, user.ID)
|
||||
}
|
||||
defer finalizeResponse(response, info, response.RealPath, user, userScope)
|
||||
if opts.SkipExtendedAttrs {
|
||||
return response, nil
|
||||
}
|
||||
// Process directory metadata if requested
|
||||
if info.Type == "directory" {
|
||||
|
||||
@@ -149,7 +149,10 @@ var types = map[string]string{
|
||||
".es": "application/x-esrehber",
|
||||
".etx": "text/x-setext",
|
||||
".evy": "application/envoy",
|
||||
".exe": "application/octet-stream",
|
||||
".exe": "application/x-msdownload",
|
||||
".dll": "application/x-msdownload",
|
||||
".bat": "application/x-msdownload",
|
||||
".msi": "application/x-msdownload",
|
||||
".f77": "text/x-fortran",
|
||||
".f90": "text/x-fortran",
|
||||
".f": "text/x-fortran",
|
||||
@@ -243,8 +246,8 @@ var types = map[string]string{
|
||||
".la": "audio/nspaudio",
|
||||
".lam": "audio/x-liveaudio",
|
||||
".latex": "application/x-latex",
|
||||
".lha": "application/lha",
|
||||
".lhx": "application/octet-stream",
|
||||
".lha": "application/x-lzh-compressed",
|
||||
".lhx": "application/x-lzh-compressed",
|
||||
".list": ContentTextHeaderValue,
|
||||
".lma": "audio/nspaudio",
|
||||
".log": ContentTextHeaderValue,
|
||||
@@ -257,7 +260,7 @@ var types = map[string]string{
|
||||
".m1v": "video/mpeg",
|
||||
".m2a": "audio/mpeg",
|
||||
".m2v": "video/mpeg",
|
||||
".m3u": "audio/x-mpegurl",
|
||||
".m3u": "text/x-mpegurl", // .m3u and .m3u8 are playlist files - They contain paths, we set it as text to allow edition.
|
||||
".m": "text/x-m",
|
||||
".man": "application/x-troff-man",
|
||||
".manifest": "text/cache-manifest",
|
||||
@@ -375,7 +378,7 @@ var types = map[string]string{
|
||||
".pre": "application/x-freelance",
|
||||
".prt": "application/pro_eng",
|
||||
".ps": "application/postscript",
|
||||
".psd": "application/octet-stream",
|
||||
".psd": "image/vnd.adobe.photoshop",
|
||||
".pvu": "paleovu/x-pv",
|
||||
".pwz": "application/vndms-powerpoint",
|
||||
".py": "text/x-scriptphyton",
|
||||
@@ -597,11 +600,63 @@ var types = map[string]string{
|
||||
".ogx": "application/ogg",
|
||||
".m4v": "video/mp4",
|
||||
".mkv": "video/x-matroska",
|
||||
".webm": "video/webm",
|
||||
".weba": "audio/webm",
|
||||
".caf": "audio/x-caf",
|
||||
".m3u8": "application/x-mpegURL",
|
||||
".m3u8": "text/x-mpegURL",
|
||||
".mpd": "application/dash+xml",
|
||||
".webp": "image/webp",
|
||||
".epub": "application/epub+zip",
|
||||
// the ones below are some from: https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
|
||||
".adp": "audio/adpcm",
|
||||
".mp4a": "audio/mp4",
|
||||
".mp2a": "audio/mpeg",
|
||||
".m3a": "audio/mpeg",
|
||||
".sil": "audio/silk",
|
||||
".uva": "audio/vnd.dece.audio",
|
||||
".uvva": "audio/vnd.dece.audio",
|
||||
".eol": "audio/vnd.digital-winds",
|
||||
".dra": "audio/vnd.dra",
|
||||
".dts": "audio/vnd.dts",
|
||||
".dtshd": "audio/vnd.dts.hd",
|
||||
".lvp": "audio/vnd.lucent.voice",
|
||||
".pya": "audio/vnd.ms-playready.media.pya",
|
||||
".rip": "audio/vnd.rip",
|
||||
".mka": "audio/x-matroska",
|
||||
".h261": "video/h261",
|
||||
".h263": "video/h263",
|
||||
".h264": "video/h264",
|
||||
".jpgv": "video/jpeg",
|
||||
".jpm": "video/jpm",
|
||||
".jpgm": "video/jpm",
|
||||
".mj2": "video/mj2",
|
||||
".mjp2": "video/mj2",
|
||||
".m2t": "video/mp2t",
|
||||
".m2ts": "video/mp2t",
|
||||
".mts": "video/mp2t",
|
||||
".mp4v": "video/mp4",
|
||||
".mpg4": "video/mp4",
|
||||
".uvh": "video/vnd.dece.hd",
|
||||
".uvvh": "video/vnd.dece.hd",
|
||||
".uvm": "video/vnd.dece.mobile",
|
||||
".uvvm": "video/vnd.dece.mobile",
|
||||
".uvp": "video/vnd.dece.pd",
|
||||
".uvvp": "video/vnd.dece.pd",
|
||||
".uvs": "video/vnd.dece.sd",
|
||||
".uvvs": "video/vnd.dece.sd",
|
||||
".uvv": "video/vnd.dece.video",
|
||||
".uvvv": "video/vnd.dece.video",
|
||||
".dvb": "video/vnd.dvb.file",
|
||||
".fvt": "video/vnd.fvt",
|
||||
".mxu": "video/vnd.mpegurl",
|
||||
".pyv": "video/vnd.ms-playready.media.pyv",
|
||||
".uvu": "video/vnd.uvvu.mp4",
|
||||
".uvvu": "video/vnd.uvvu.mp4",
|
||||
".f4v": "video/x-f4v",
|
||||
".mk3d": "video/x-matroska",
|
||||
".mks": "video/x-matroska",
|
||||
".vob": "video/x-ms-vob",
|
||||
".smv": "video/x-smv",
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
|
||||
@@ -22,6 +22,7 @@ var (
|
||||
ErrNoTotpConfigured = errors.New("OTP is enforced, but user is not yet configured")
|
||||
ErrUnauthorized = errors.New("user unauthorized")
|
||||
ErrNotIndexed = errors.New("directory or item excluded from indexing")
|
||||
ErrNotViewable = errors.New("directory or item is not viewable")
|
||||
ErrWrongLoginMethod = errors.New("user attempted to login with wrong login method")
|
||||
ErrDownloadNotAllowed = errors.New("downloads are not allowed for this share")
|
||||
ErrUploadNotAllowed = errors.New("upload permission not allowed for this share")
|
||||
|
||||
@@ -667,14 +667,9 @@ func setDefaults(generate bool) Settings {
|
||||
database := os.Getenv("FILEBROWSER_DATABASE")
|
||||
if database == "" {
|
||||
database = "database.db"
|
||||
} else {
|
||||
// check if database file exists
|
||||
if _, err := os.Stat(database); os.IsNotExist(err) {
|
||||
database = "database.db"
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(database); os.IsNotExist(err) {
|
||||
logger.Warning("database file could not be found. If this is unexpected, the default path is `./database.db`, but it can be configured in the config file under `server.database`.")
|
||||
logger.Warning("database file could not be found. If this is unexpected, please set the FILEBROWSER_DATABASE environment variable to the correct path.")
|
||||
}
|
||||
s := Settings{
|
||||
Server: Server{
|
||||
@@ -712,8 +707,10 @@ func setDefaults(generate bool) Settings {
|
||||
Name: "FileBrowser Quantum",
|
||||
},
|
||||
UserDefaults: UserDefaults{
|
||||
DeleteAfterArchive: true,
|
||||
DisableOnlyOfficeExt: ".md .txt .pdf .html .xml",
|
||||
StickySidebar: true,
|
||||
HideFilesInTree: false,
|
||||
LockPassword: false,
|
||||
ShowHidden: false,
|
||||
DarkMode: boolPtr(true),
|
||||
@@ -732,11 +729,13 @@ func setDefaults(generate bool) Settings {
|
||||
Preview: UserDefaultsPreview{
|
||||
Image: boolPtr(true),
|
||||
Video: boolPtr(true),
|
||||
Audio: boolPtr(true),
|
||||
MotionVideoPreview: boolPtr(true),
|
||||
Office: boolPtr(true),
|
||||
PopUp: boolPtr(true),
|
||||
AutoplayMedia: boolPtr(true),
|
||||
Folder: boolPtr(true),
|
||||
Models: boolPtr(true),
|
||||
},
|
||||
FileLoading: users.FileLoading{
|
||||
MaxConcurrent: 10,
|
||||
|
||||
@@ -68,6 +68,8 @@ func AdminPerms() users.Permissions {
|
||||
func ApplyUserDefaults(u *users.User) {
|
||||
u.StickySidebar = Config.UserDefaults.StickySidebar
|
||||
u.DisableSettings = Config.UserDefaults.DisableSettings
|
||||
u.HideFilesInTree = Config.UserDefaults.HideFilesInTree
|
||||
u.DeleteAfterArchive = Config.UserDefaults.DeleteAfterArchive
|
||||
|
||||
// Handle DarkMode with default - dereference pointer from config
|
||||
u.DarkMode = boolValueOrDefault(Config.UserDefaults.DarkMode, true)
|
||||
@@ -90,12 +92,14 @@ func ApplyUserDefaults(u *users.User) {
|
||||
u.Preview.DisableHideSidebar = Config.UserDefaults.Preview.DisableHideSidebar
|
||||
u.Preview.Image = boolValueOrDefault(Config.UserDefaults.Preview.Image, true)
|
||||
u.Preview.Video = boolValueOrDefault(Config.UserDefaults.Preview.Video, true)
|
||||
u.Preview.Audio = boolValueOrDefault(Config.UserDefaults.Preview.Audio, true)
|
||||
u.Preview.MotionVideoPreview = boolValueOrDefault(Config.UserDefaults.Preview.MotionVideoPreview, true)
|
||||
u.Preview.Office = boolValueOrDefault(Config.UserDefaults.Preview.Office, true)
|
||||
u.Preview.PopUp = boolValueOrDefault(Config.UserDefaults.Preview.PopUp, true)
|
||||
u.Preview.AutoplayMedia = boolValueOrDefault(Config.UserDefaults.Preview.AutoplayMedia, true)
|
||||
u.Preview.DefaultMediaPlayer = Config.UserDefaults.Preview.DefaultMediaPlayer
|
||||
u.Preview.Folder = boolValueOrDefault(Config.UserDefaults.Preview.Folder, true)
|
||||
u.Preview.Models = boolValueOrDefault(Config.UserDefaults.Preview.Models, true)
|
||||
|
||||
u.ShowHidden = Config.UserDefaults.ShowHidden
|
||||
u.DateFormat = Config.UserDefaults.DateFormat
|
||||
@@ -107,7 +111,7 @@ func ApplyUserDefaults(u *users.User) {
|
||||
u.DisableOnlyOfficeExt = Config.UserDefaults.DisableOnlyOfficeExt
|
||||
u.FileLoading = Config.UserDefaults.FileLoading
|
||||
u.DisableOfficePreviewExt = Config.UserDefaults.DisableOfficePreviewExt
|
||||
if len(u.Scopes) == 0 {
|
||||
if len(u.Scopes) == 0 && u.Username != "anonymous" {
|
||||
for _, source := range Config.Server.Sources {
|
||||
if source.Config.DefaultEnabled {
|
||||
u.Scopes = append(u.Scopes, users.SourceScope{
|
||||
|
||||
@@ -306,12 +306,14 @@ type UserDefaultsPreview struct {
|
||||
HighQuality *bool `json:"highQuality"` // deprecated: always true in v1.3.0+
|
||||
Image *bool `json:"image"` // show thumbnails for image files
|
||||
Video *bool `json:"video"` // show thumbnails for video files
|
||||
Audio *bool `json:"audio"` // show thumbnails for audio files
|
||||
MotionVideoPreview *bool `json:"motionVideoPreview"` // show multiple frames for videos in thumbnail preview when hovering
|
||||
Office *bool `json:"office"` // show thumbnails for office files
|
||||
PopUp *bool `json:"popup"` // show larger popup preview when hovering over thumbnail
|
||||
AutoplayMedia *bool `json:"autoplayMedia"` // autoplay media files in preview
|
||||
DefaultMediaPlayer bool `json:"defaultMediaPlayer"` // disable the styled feature-rich media player for browser default
|
||||
Folder *bool `json:"folder"` // show thumbnails for folders that have previewable contents
|
||||
Models *bool `json:"models"` // show live thumbnails for 3D models files
|
||||
}
|
||||
|
||||
// UserDefaultsPermissions holds permission settings with pointer types for defaults
|
||||
@@ -334,6 +336,7 @@ type UserDefaults struct {
|
||||
DisableQuickToggles bool `json:"disableQuickToggles"` // disable the quick toggles in the sidebar
|
||||
DisableSearchOptions bool `json:"disableSearchOptions"` // disable the search options in the search bar
|
||||
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
|
||||
HideFilesInTree bool `json:"hideFilesInTree"` // hide files in the sidebar tree navigation, when true, will show only directories.
|
||||
DarkMode *bool `json:"darkMode"` // should dark mode be enabled
|
||||
Locale string `json:"locale"` // language to use: eg. de, en, or fr
|
||||
ViewMode string `json:"viewMode"` // view mode to use: eg. normal, list, grid, or compact
|
||||
@@ -353,6 +356,7 @@ type UserDefaults struct {
|
||||
LoginMethod string `json:"loginMethod,omitempty"` // login method to use: eg. password, proxy, oidc
|
||||
DisableUpdateNotifications bool `json:"disableUpdateNotifications"` // disable update notifications banner for admin users
|
||||
DeleteWithoutConfirming bool `json:"deleteWithoutConfirming"` // delete files without confirmation
|
||||
DeleteAfterArchive bool `json:"deleteAfterArchive"` // delete source files after successful creation/extraction of archives
|
||||
FileLoading users.FileLoading `json:"fileLoading"` // upload and download settings
|
||||
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // deprecated: use disablePreviewExt instead
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // list of file extensions to disable onlyoffice editor for
|
||||
|
||||
@@ -60,6 +60,7 @@ type FileOptions struct {
|
||||
FollowSymlinks bool // whether to follow symlinks
|
||||
Only string // whether to only get files or folders
|
||||
SkipExtendedAttrs bool // whether to skip extended attributes
|
||||
ShowSharedAttr bool // whether to show share attributes
|
||||
}
|
||||
|
||||
// SanitizeUserPath prevents path traversal attacks by cleaning and validating user input.
|
||||
|
||||
@@ -8,3 +8,4 @@ userDefaults:
|
||||
image: true
|
||||
popup: true
|
||||
video: true
|
||||
audio: true
|
||||
+5
-1
@@ -11,6 +11,7 @@ userDefaults:
|
||||
disableQuickToggles: false # disable the quick toggles in the sidebar
|
||||
disableSearchOptions: false # disable the search options in the search bar
|
||||
stickySidebar: true # keep sidebar open when navigating
|
||||
hideFilesInTree: false # hide files in the sidebar tree navigation, when true, will show only directories.
|
||||
darkMode: true # should dark mode be enabled (defaults to true if not set)
|
||||
viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact
|
||||
singleClick: false # open directory on single click, also enables middle click to open in new tab
|
||||
@@ -27,12 +28,14 @@ userDefaults:
|
||||
disableHideSidebar: false # disable the hide sidebar preview for previews and editors
|
||||
image: true # show thumbnail preview image for image files (defaults to true if not set)
|
||||
video: true # show thumbnail preview image for video files (defaults to true if not set)
|
||||
audio: true # show thumbnail preview image for audio files (defaults to true if not set)
|
||||
motionVideoPreview: true # show multiple frames for videos in thumbnail preview when hovering (defaults to true if not set)
|
||||
office: true # show thumbnail preview image for office files (defaults to true if not set)
|
||||
popup: true # show larger popup preview when hovering over thumbnail (defaults to true if not set)
|
||||
autoplayMedia: true # autoplay media files in preview (defaults to true if not set)
|
||||
defaultMediaPlayer: false # disable html5 media player and use the default media player
|
||||
folder: true # show thumbnail preview image for folder files (defaults to true if not set)
|
||||
models: true # show live thumbnail preview for 3D Models files (defaults to true if not set)
|
||||
permissions:
|
||||
api: false # allow api access
|
||||
admin: false # allow admin access
|
||||
@@ -44,7 +47,8 @@ userDefaults:
|
||||
download: true # allow downloading files (defaults to true if not set)
|
||||
disableUpdateNotifications: false # disable update notifications banner for admin users
|
||||
deleteWithoutConfirming: false # delete files without confirmation
|
||||
deleteAfterArchive: true # delete source files after successful creation/extraction of archives
|
||||
disableOnlyOfficeExt: ".md .txt .pdf .html .xml" # list of file extensions to disable onlyoffice editor for
|
||||
customTheme: "" # Name of theme to use chosen from custom themes config.
|
||||
showSelectMultiple: false # show select multiple files on desktop
|
||||
debugOffice: false # debug onlyoffice editor
|
||||
debugOffice: false # debug onlyoffice editor
|
||||
@@ -1,11 +1,12 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
// IsSingleFileShare determines if this share is for a single file (not a directory).
|
||||
@@ -144,10 +145,28 @@ func (l *Link) HasReachedUserLimit(username string) bool {
|
||||
return count >= l.DownloadsLimit
|
||||
}
|
||||
|
||||
func (l *Link) GetSourceName() (string, error) {
|
||||
func (l *Link) GetSourceName() string {
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[l.Source]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("source not found")
|
||||
return ""
|
||||
}
|
||||
return sourceInfo.Name, nil
|
||||
return sourceInfo.Name
|
||||
}
|
||||
|
||||
func (l *Link) UserCanEdit(user *users.User) bool {
|
||||
return l.UserID == user.ID || user.Permissions.Admin
|
||||
}
|
||||
|
||||
func (l *Link) SourceURL(user *users.User) string {
|
||||
sourceName := l.GetSourceName()
|
||||
// get user scope path from share
|
||||
userScope, err := user.GetScopeForSourceName(sourceName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(l.Path, userScope) {
|
||||
return ""
|
||||
}
|
||||
scopedPath := strings.TrimPrefix(l.Path, userScope)
|
||||
return "/" + filepath.Join("files", sourceName, scopedPath)
|
||||
}
|
||||
@@ -45,6 +45,8 @@ type CommonShare struct {
|
||||
HasPassword bool `json:"hasPassword,omitempty"`
|
||||
ShowHidden bool `json:"showHidden,omitempty"` // show hidden files in share (true = show, false = hide)
|
||||
DisableLoginOption bool `json:"disableLoginOption"` // disable login option in share (true = hide, false = show)
|
||||
SourceURL string `json:"sourceURL,omitempty"`
|
||||
CanEditShare bool `json:"canEditShare,omitempty"`
|
||||
}
|
||||
type CreateBody struct {
|
||||
CommonShare
|
||||
|
||||
@@ -309,8 +309,13 @@ func (s *Storage) GetBySourcePath(path, source string) ([]*Link, error) {
|
||||
}
|
||||
|
||||
// IsShared returns whether the given path and source have any shares in the cache.
|
||||
func (s *Storage) IsShared(path, source string) bool {
|
||||
func (s *Storage) IsShared(path, source string, id uint) bool {
|
||||
links, _ := s.GetBySourcePath(path, source)
|
||||
for _, l := range links {
|
||||
if l.UserID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return len(links) > 0
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
var userStore *users.Storage
|
||||
|
||||
func InitializeDb(path string) (*bolt.BoltStore, bool, error) {
|
||||
logger.Debugf("Initializing database: %v", path)
|
||||
exists, err := dbExists(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -56,12 +56,14 @@ type Preview struct {
|
||||
DisableHideSidebar bool `json:"disableHideSidebar"` // disable the hide sidebar preview for previews and editors
|
||||
Image bool `json:"image"` // show thumbnail preview image for image files
|
||||
Video bool `json:"video"` // show thumbnail preview image for video files
|
||||
Audio bool `json:"audio"` // show thumbnail preview image for audio files
|
||||
MotionVideoPreview bool `json:"motionVideoPreview"` // show multiple frames for videos in thumbnail preview when hovering
|
||||
Office bool `json:"office"` // show thumbnail preview image for office files
|
||||
PopUp bool `json:"popup"` // show larger popup preview when hovering over thumbnail
|
||||
AutoplayMedia bool `json:"autoplayMedia"` // autoplay media files in preview
|
||||
DefaultMediaPlayer bool `json:"defaultMediaPlayer"` // disable html5 media player and use the default media player
|
||||
Folder bool `json:"folder"` // show thumbnail preview image for folder files
|
||||
Models bool `json:"models"` // show live thumbnails for 3D models files
|
||||
}
|
||||
|
||||
// User describes a user.
|
||||
@@ -122,6 +124,8 @@ type NonAdminEditable struct {
|
||||
DebugOffice bool `json:"debugOffice"` // debug onlyoffice editor
|
||||
OtpEnabled bool `json:"otpEnabled"` // allow non-admin users to disable their own OTP
|
||||
SidebarLinks []SidebarLink `json:"sidebarLinks"` // customizable sidebar links
|
||||
HideFilesInTree bool `json:"hideFilesInTree"` // hide files in the sidebar tree navigation, when true, will show only directories.
|
||||
DeleteAfterArchive bool `json:"deleteAfterArchive"` // delete source files after successful creation/extraction of archives
|
||||
}
|
||||
|
||||
type FileLoading struct {
|
||||
|
||||
+17
-17
@@ -16,7 +16,7 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gtsteffaniak/go-cache v1.1.0
|
||||
github.com/gtsteffaniak/go-logger v1.0.2
|
||||
github.com/kovidgoyal/imaging v1.8.19
|
||||
github.com/kovidgoyal/imaging v1.8.20
|
||||
github.com/mattn/go-sqlite3 v1.14.34
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/spf13/afero v1.15.0
|
||||
@@ -26,13 +26,13 @@ require (
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/image v0.36.0
|
||||
golang.org/x/mod v0.33.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/time v0.14.0
|
||||
golang.org/x/tools v0.42.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.45.0
|
||||
modernc.org/sqlite v1.46.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -101,7 +101,7 @@ require (
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0-alpha.4 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/ettle/strcase v0.2.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
@@ -114,16 +114,16 @@ require (
|
||||
github.com/go-critic/go-critic v0.14.3 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/spec v0.22.3 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.5 // indirect
|
||||
github.com/go-openapi/spec v0.22.4 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
@@ -139,7 +139,7 @@ require (
|
||||
github.com/godoc-lint/godoc-lint v0.11.2 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/gohugoio/hugo v0.149.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20260129164528-943061e2742c // indirect
|
||||
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golangci/asciicheck v0.5.0 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
|
||||
@@ -273,7 +273,7 @@ require (
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
@@ -281,7 +281,7 @@ require (
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
honnef.co/go/tools v0.7.0 // indirect
|
||||
modernc.org/libc v1.68.0 // indirect
|
||||
modernc.org/libc v1.69.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
mvdan.cc/gofumpt v0.9.2 // indirect
|
||||
|
||||
+44
-44
@@ -248,8 +248,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.10.0-alpha.4 h1:JzPbdf+cqbyT9sZtP4xnqelwUXwf7LvD8xKS6+ofTds=
|
||||
github.com/ebitengine/purego v0.10.0-alpha.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -306,33 +306,33 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
|
||||
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
|
||||
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
|
||||
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
|
||||
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
||||
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
|
||||
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
|
||||
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
|
||||
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
|
||||
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
|
||||
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
|
||||
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
|
||||
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
|
||||
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
|
||||
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
|
||||
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
|
||||
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
|
||||
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
|
||||
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -401,8 +401,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20260129164528-943061e2742c h1:ysO2h2Odnl1AJM1I2Lm/fa6JvO0pECMSt2CwBaa+ITo=
|
||||
github.com/golang/geo v0.0.0-20260129164528-943061e2742c/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
||||
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a h1:7f/cr+n7Jtd7mzCLrx47sdVIGabpSSxCHrf6fJczDp0=
|
||||
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -580,8 +580,8 @@ github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811ui
|
||||
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
|
||||
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
|
||||
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
|
||||
github.com/kovidgoyal/imaging v1.8.19 h1:zWJdQqF2tfSKjvoB7XpLRhVGbYsze++M0iaqZ4ZkhNk=
|
||||
github.com/kovidgoyal/imaging v1.8.19/go.mod h1:I0q8RdoEuyc4G8GFOF9CaluTUHQSf68d6TmsqpvfRI8=
|
||||
github.com/kovidgoyal/imaging v1.8.20 h1:74GZ7C2rIm3rqmGEjK1GvvPOOnJ0SS5iDOa6Flfo0b0=
|
||||
github.com/kovidgoyal/imaging v1.8.20/go.mod h1:d3phGYkTChGYkY4y++IjpHgUGhWGELDc2NEQAqxwZZg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -940,8 +940,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk=
|
||||
@@ -1019,8 +1019,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1293,18 +1293,18 @@ honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
|
||||
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
|
||||
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/ccgo/v4 v4.31.0 h1:/bsaxqdgX3gy/0DboxcvWrc3NpzH+6wpFfI/ZaA/hrg=
|
||||
modernc.org/ccgo/v4 v4.31.0/go.mod h1:jKe8kPBjIN/VdGTVqARTQ8N1gAziBmiISY8j5HoKwjg=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
|
||||
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
||||
modernc.org/libc v1.69.0 h1:YQJ5QMSReTgQ3QFmI0dudfjXIjCcYTUxcH8/9P9f0D8=
|
||||
modernc.org/libc v1.69.0/go.mod h1:YfLLduUEbodNV2xLU5JOnRHBTAHVHsVW3bVYGw0ZCV4=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -1313,8 +1313,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs=
|
||||
modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
+17
-60
@@ -344,22 +344,10 @@ func addFile(source string, path string, d *requestContext, tarWriter *tar.Write
|
||||
}
|
||||
|
||||
// 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 !store.Access.Permitted(idx.Path, path, d.user.Username) {
|
||||
return nil // Silently skip this file/folder
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
_, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
FollowSymlinks: true,
|
||||
}, store.Access, d.user, store.Share)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
realPath, _, _ := idx.GetRealPath(path)
|
||||
info, err := os.Stat(realPath)
|
||||
if err != nil {
|
||||
@@ -490,27 +478,11 @@ func computeArchiveSize(source string, fileList []string, d *requestContext) (in
|
||||
return 0, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
|
||||
var userScope string
|
||||
var err error
|
||||
if d.share == nil {
|
||||
userScope, err = d.user.GetScopeForSourceName(source)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
|
||||
}
|
||||
}
|
||||
|
||||
for _, path := range fileList {
|
||||
var fullPath string
|
||||
if d.share == nil {
|
||||
fullPath = utils.JoinPathAsUnix(userScope, path)
|
||||
if store.Access != nil && !store.Access.Permitted(idx.Path, fullPath, d.user.Username) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
fullPath = path
|
||||
if !store.Access.Permitted(idx.Path, path, d.user.Username) {
|
||||
continue
|
||||
}
|
||||
|
||||
realPath, isDir, err := idx.GetRealPath(fullPath)
|
||||
realPath, isDir, err := idx.GetRealPath(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -537,10 +509,10 @@ func createZip(d *requestContext, source string, tmpPath string, filenames ...st
|
||||
|
||||
zipWriter := zip.NewWriter(file)
|
||||
|
||||
for _, fname := range filenames {
|
||||
err := addFile(source, fname, d, nil, zipWriter, false)
|
||||
for _, filepath := range filenames {
|
||||
err := addFile(source, filepath, d, nil, zipWriter, false)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to add %s to ZIP: %v", fname, err)
|
||||
logger.Errorf("Failed to add %s to ZIP: %v", filepath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -562,10 +534,10 @@ func createTarGz(d *requestContext, source string, tmpPath string, filenames ...
|
||||
gzWriter := gzip.NewWriter(file)
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
|
||||
for _, fname := range filenames {
|
||||
err := addFile(source, fname, d, tarWriter, nil, false)
|
||||
for _, filepath := range filenames {
|
||||
err := addFile(source, filepath, d, tarWriter, nil, false)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to add %s to TAR.GZ: %v", fname, err)
|
||||
logger.Errorf("Failed to add %s to TAR.GZ: %v", filepath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -600,10 +572,10 @@ func createTarGzWithLevel(d *requestContext, source string, destPath string, lev
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
for _, fname := range filenames {
|
||||
err := addFile(source, fname, d, tarWriter, nil, false)
|
||||
for _, filepath := range filenames {
|
||||
err := addFile(source, filepath, d, tarWriter, nil, false)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to add %s to TAR.GZ: %v", fname, err)
|
||||
logger.Errorf("Failed to add %s to TAR.GZ: %v", filepath, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -616,32 +588,18 @@ func createTarGzWithLevel(d *requestContext, source string, destPath string, lev
|
||||
// BuildAndStreamArchive resolves paths, creates a zip or tar.gz archive, and streams it to w.
|
||||
// It respects access rules and max archive size. Used only by the raw handler for multi-file/directory download.
|
||||
func BuildAndStreamArchive(w http.ResponseWriter, r *http.Request, d *requestContext, source string, fileList []string) (int, error) {
|
||||
firstFilePath := fileList[0]
|
||||
var userscope string
|
||||
var err error
|
||||
|
||||
if d.share == nil {
|
||||
userscope, err = d.user.GetScopeForSourceName(source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
firstFilePath = utils.JoinPathAsUnix(userscope, firstFilePath)
|
||||
}
|
||||
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
realPath, isDir, err := idx.GetRealPath(firstFilePath)
|
||||
realPath, isDir, err := idx.GetRealPath(fileList[0])
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to get real path for %s: %v", fileList[0], err)
|
||||
}
|
||||
|
||||
estimatedSize, err := computeArchiveSize(source, fileList, d)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
if config.Server.MaxArchiveSizeGB > 0 {
|
||||
maxSize := config.Server.MaxArchiveSizeGB * 1024 * 1024 * 1024
|
||||
if estimatedSize > maxSize {
|
||||
@@ -660,7 +618,7 @@ func BuildAndStreamArchive(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
return http.StatusInternalServerError, errors.New("format not implemented")
|
||||
}
|
||||
|
||||
baseDirName := filepath.Base(filepath.Dir(firstFilePath))
|
||||
baseDirName := filepath.Base(filepath.Dir(realPath))
|
||||
if baseDirName == "" || baseDirName == "/" {
|
||||
baseDirName = "download"
|
||||
}
|
||||
@@ -711,7 +669,6 @@ func BuildAndStreamArchive(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
_, err = io.Copy(w, reader)
|
||||
os.Remove(archiveData)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to copy archive data to response: %v", err)
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return 0, nil
|
||||
|
||||
@@ -139,6 +139,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
var documentId string
|
||||
var logContext *OnlyOfficeLogContext
|
||||
|
||||
// modify all filepaths for user scope
|
||||
if d.share == nil {
|
||||
userscope, err = d.user.GetScopeForSourceName(source)
|
||||
if err != nil {
|
||||
@@ -160,8 +161,11 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
}
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
firstFilePath = utils.JoinPathAsUnix(userscope, firstFilePath)
|
||||
for i, filePath := range fileList {
|
||||
fileList[i] = utils.JoinPathAsUnix(userscope, filePath)
|
||||
}
|
||||
}
|
||||
firstFilePath = fileList[0]
|
||||
// For shares, the path is already correctly resolved by publicRawHandler
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -45,6 +46,8 @@ func (u *userInfoUnmarshaller) UnmarshalJSON(data []byte) error {
|
||||
if u.groupsClaim != "" {
|
||||
if v, ok := raw[u.groupsClaim]; ok {
|
||||
switch val := v.(type) {
|
||||
case map[string]interface{}:
|
||||
u.userInfo.Groups = slices.Collect(maps.Keys(val))
|
||||
case []interface{}:
|
||||
groups := make([]string, len(val))
|
||||
for i, g := range val {
|
||||
|
||||
@@ -119,6 +119,7 @@ func getDirectoryPreview(r *http.Request, d *requestContext, frameIndex int) (*i
|
||||
|
||||
source := d.fileInfo.Source
|
||||
path := utils.JoinPathAsUnix(d.fileInfo.Path, name)
|
||||
user := d.user
|
||||
if d.share != nil {
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
|
||||
if !ok {
|
||||
@@ -126,6 +127,7 @@ func getDirectoryPreview(r *http.Request, d *requestContext, frameIndex int) (*i
|
||||
}
|
||||
source = sourceInfo.Name
|
||||
path = d.IndexPath + name
|
||||
user = d.shareUser
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(
|
||||
utils.FileOptions{
|
||||
@@ -133,7 +135,7 @@ func getDirectoryPreview(r *http.Request, d *requestContext, frameIndex int) (*i
|
||||
Source: source,
|
||||
AlbumArt: true,
|
||||
Metadata: true,
|
||||
}, store.Access, d.user, store.Share)
|
||||
}, store.Access, user, store.Share)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -245,8 +245,8 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if !d.share.AllowModify {
|
||||
return http.StatusForbidden, fmt.Errorf("create is not allowed for this share")
|
||||
}
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
sourceName := d.share.GetSourceName()
|
||||
if sourceName == "" {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
|
||||
resolvedPath := utils.JoinPathAsUnix(d.share.Path, cleanPath)
|
||||
err = files.WriteFile(source, resolvedPath, r.Body)
|
||||
err = files.WriteFile(sourceName, resolvedPath, r.Body)
|
||||
// hide the error
|
||||
if err != nil {
|
||||
logger.Errorf("public put handler: error updating resource with error %v", err)
|
||||
@@ -343,8 +343,8 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
}
|
||||
|
||||
// Get the source from the share
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
sourceName := d.share.GetSourceName()
|
||||
if sourceName == "" {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
|
||||
@@ -370,9 +370,9 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// Note: Share paths are absolute, so we don't strip user scope here
|
||||
// resourcePatchHandler will skip adding scope for shares
|
||||
for i := range req.Items {
|
||||
req.Items[i].FromSource = source
|
||||
req.Items[i].FromSource = sourceName
|
||||
req.Items[i].FromPath = utils.JoinPathAsUnix(d.share.Path, req.Items[i].FromPath)
|
||||
req.Items[i].ToSource = source
|
||||
req.Items[i].ToSource = sourceName
|
||||
req.Items[i].ToPath = utils.JoinPathAsUnix(d.share.Path, req.Items[i].ToPath)
|
||||
}
|
||||
d.Data = req
|
||||
|
||||
@@ -86,6 +86,7 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
ExtractEmbeddedSubtitles: settings.Config.Integrations.Media.ExtractEmbeddedSubtitles,
|
||||
ShowHidden: d.user.ShowHidden,
|
||||
SkipExtendedAttrs: skipExtendedAttrs,
|
||||
ShowSharedAttr: true,
|
||||
}, store.Access, d.user, store.Share)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@@ -255,12 +256,12 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
}
|
||||
|
||||
if d.share != nil {
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
sourceName := d.share.GetSourceName()
|
||||
if sourceName == "" {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
// get user scope path from share
|
||||
userScope, err := d.shareUser.GetScopeForSourceName(source)
|
||||
userScope, err := d.shareUser.GetScopeForSourceName(sourceName)
|
||||
if err != nil {
|
||||
response.Failed = append(response.Failed, BulkDeleteItem{
|
||||
Source: item.Source,
|
||||
@@ -275,7 +276,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
FollowSymlinks: true,
|
||||
Path: indexPath,
|
||||
Source: source,
|
||||
Source: sourceName,
|
||||
ShowHidden: true,
|
||||
}, store.Access, filePermUser, store.Share)
|
||||
if err != nil {
|
||||
@@ -283,7 +284,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
}
|
||||
|
||||
// Delete the file/directory
|
||||
err = files.DeleteFiles(source, fileInfo.RealPath, fileInfo.Type == "directory")
|
||||
err = files.DeleteFiles(sourceName, fileInfo.RealPath, fileInfo.Type == "directory")
|
||||
if err != nil {
|
||||
logger.Errorf("resource bulk delete handler: error deleting file/directory: %v", err)
|
||||
response.Failed = append(response.Failed, BulkDeleteItem{
|
||||
|
||||
@@ -83,6 +83,11 @@ func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
searchSize := indexing.DefaultSearchResults
|
||||
if searchOptions.largest {
|
||||
searchSize = 200
|
||||
}
|
||||
|
||||
var response []*indexing.SearchResult
|
||||
if len(searchOptions.sources) == 1 {
|
||||
// Single source - use the existing Search method for backward compatibility
|
||||
@@ -91,10 +96,10 @@ func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
return http.StatusBadRequest, fmt.Errorf("index not found for source %s", searchOptions.sources[0])
|
||||
}
|
||||
combinedPath := searchOptions.combinedPath[searchOptions.sources[0]]
|
||||
response = index.Search(searchOptions.query, combinedPath, searchOptions.sessionId, searchOptions.largest, indexing.DefaultSearchResults)
|
||||
response = index.Search(searchOptions.query, combinedPath, searchOptions.sessionId, searchOptions.largest, searchSize)
|
||||
} else {
|
||||
// Multiple sources - use the new SearchMultiSources function
|
||||
response = indexing.SearchMultiSources(searchOptions.query, searchOptions.sources, searchOptions.combinedPath, searchOptions.sessionId, searchOptions.largest, indexing.DefaultSearchResults)
|
||||
response = indexing.SearchMultiSources(searchOptions.query, searchOptions.sources, searchOptions.combinedPath, searchOptions.sessionId, searchOptions.largest, searchSize)
|
||||
}
|
||||
|
||||
// Filter out items that are not permitted according to access rules and trim user scope from paths
|
||||
|
||||
+40
-12
@@ -33,14 +33,10 @@ type ShareResponse struct {
|
||||
}
|
||||
|
||||
// convertToFrontendShareResponse converts shares to response format with usernames
|
||||
func convertToFrontendShareResponse(r *http.Request, shares []*share.Link) ([]*ShareResponse, error) {
|
||||
func convertToFrontendShareResponse(r *http.Request, shares []*share.Link, user *users.User) ([]*ShareResponse, error) {
|
||||
responses := make([]*ShareResponse, 0, len(shares))
|
||||
for _, s := range shares {
|
||||
user, err := store.Users.Get(s.UserID)
|
||||
username := ""
|
||||
if err == nil {
|
||||
username = user.Username
|
||||
}
|
||||
username := user.Username
|
||||
|
||||
// Get source info to convert path to name for frontend
|
||||
sourceInfo, ok := config.Server.SourceMap[s.Source]
|
||||
@@ -62,6 +58,9 @@ func convertToFrontendShareResponse(r *http.Request, shares []*share.Link) ([]*S
|
||||
s.CommonShare.HasPassword = s.HasPassword()
|
||||
s.DownloadURL = getShareURL(r, s.Hash, true, s.Token)
|
||||
s.ShareURL = getShareURL(r, s.Hash, false, s.Token)
|
||||
if s.UserCanEdit(user) {
|
||||
s.CommonShare.SourceURL = s.SourceURL(user)
|
||||
}
|
||||
// Create response with source name (overrides the embedded Link's source field)
|
||||
responses = append(responses, &ShareResponse{
|
||||
Link: s,
|
||||
@@ -94,7 +93,7 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
shares = utils.NonNilSlice(shares)
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, shares)
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, shares, d.user)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -134,7 +133,7 @@ func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
|
||||
}
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, s)
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, s, d.user)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -159,7 +158,16 @@ func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
|
||||
err := store.Share.Delete(hash)
|
||||
// only allow users to delete their own shares
|
||||
thisShare, err := store.Share.GetByHash(hash)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("share not found")
|
||||
}
|
||||
if thisShare.UserID != d.user.ID && !d.user.Permissions.Admin {
|
||||
return http.StatusForbidden, fmt.Errorf("you are not allowed to delete this share")
|
||||
}
|
||||
|
||||
err = store.Share.Delete(hash)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -193,8 +201,16 @@ func sharePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext
|
||||
return http.StatusBadRequest, fmt.Errorf("hash and path are required")
|
||||
}
|
||||
|
||||
// only allow users to update their own shares
|
||||
thisShare, err := store.Share.GetByHash(body.Hash)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("share not found")
|
||||
}
|
||||
if thisShare.UserID != d.user.ID && !d.user.Permissions.Admin {
|
||||
return http.StatusForbidden, fmt.Errorf("you are not allowed to update this share")
|
||||
}
|
||||
// Update the share path
|
||||
err := store.Share.UpdateSharePath(body.Hash, body.Path)
|
||||
err = store.Share.UpdateSharePath(body.Hash, body.Path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -206,7 +222,7 @@ func sharePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{updatedShare})
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{updatedShare}, d.user)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -386,7 +402,7 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if err = store.Share.Save(s); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{s})
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{s}, d.user)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -618,6 +634,7 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
commonShare.Source = ""
|
||||
commonShare.Path = ""
|
||||
commonShare.DownloadURL = ""
|
||||
commonShare.SidebarLinks = []users.SidebarLink{}
|
||||
for _, link := range shareLink.SidebarLinks {
|
||||
if link.Category == "download" && shareLink.ShareType == "upload" {
|
||||
@@ -626,6 +643,17 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
commonShare.SidebarLinks = append(commonShare.SidebarLinks, link)
|
||||
}
|
||||
}
|
||||
commonShare.SourceURL = shareLink.SourceURL(d.user)
|
||||
commonShare.CanEditShare = shareLink.UserCanEdit(d.user)
|
||||
if commonShare.SourceURL != "" {
|
||||
link := users.SidebarLink{
|
||||
Name: "sourceLocation",
|
||||
Category: "custom",
|
||||
Target: commonShare.SourceURL,
|
||||
}
|
||||
commonShare.SidebarLinks = append(commonShare.SidebarLinks, link)
|
||||
}
|
||||
|
||||
return renderJSON(w, r, commonShare)
|
||||
}
|
||||
|
||||
|
||||
+17
-9
@@ -3,12 +3,12 @@ package http
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
@@ -25,10 +25,18 @@ type TemplateRenderer struct {
|
||||
|
||||
// Render renders a template document with headers and data
|
||||
func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data interface{}) error {
|
||||
// If in dev mode, reload templates on every render.
|
||||
templates := t.templates
|
||||
// If in dev mode, reload templates on every render by creating a fresh instance
|
||||
if t.devMode {
|
||||
var err error
|
||||
t.templates, err = t.templates.ParseFS(assetFs, "public/index.html")
|
||||
templates = template.New("").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface{}) (string, error) {
|
||||
var a []byte
|
||||
a, err = json.Marshal(v)
|
||||
return string(a), err
|
||||
},
|
||||
})
|
||||
templates, err = templates.ParseFS(assetFs, "public/index.html")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reloading template: %w", err)
|
||||
}
|
||||
@@ -39,7 +47,7 @@ func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data inter
|
||||
w.Header().Set("X-Accel-Expires", "0")
|
||||
w.Header().Set("Transfer-Encoding", "identity")
|
||||
// Execute the template with the provided data
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
return templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestContext, file, contentType string) (int, error) {
|
||||
@@ -180,8 +188,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
|
||||
data["htmlVars"] = map[string]interface{}{
|
||||
"title": title,
|
||||
"customCSS": config.Frontend.Styling.CustomCSSRaw,
|
||||
"userSelectedTheme": userSelectedTheme,
|
||||
"customCSS": template.CSS(config.Frontend.Styling.CustomCSSRaw),
|
||||
"userSelectedTheme": template.CSS(userSelectedTheme),
|
||||
"lightBackground": config.Frontend.Styling.LightBackground,
|
||||
"darkBackground": config.Frontend.Styling.DarkBackground,
|
||||
"staticURL": staticURL,
|
||||
@@ -192,7 +200,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
"winIcon": staticURL + "/mstile-256x256.png",
|
||||
"appIcon": staticURL + "/apple-touch-icon.png",
|
||||
"description": description,
|
||||
"loadingSpinnersCSS": loadingSpinnersCSS,
|
||||
"loadingSpinnersCSS": template.CSS(loadingSpinnersCSS),
|
||||
"banner": banner,
|
||||
"image": ogImage,
|
||||
"url": fullURL,
|
||||
@@ -241,8 +249,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
return http.StatusInternalServerError, fmt.Errorf("error marshaling globalVars: %w", err)
|
||||
}
|
||||
|
||||
// Replace with JSON strings for direct template usage
|
||||
data["globalVars"] = string(globalVarsJSON)
|
||||
// Mark as JS for html/template to avoid escaping
|
||||
data["globalVars"] = template.JS(globalVarsJSON)
|
||||
|
||||
// Render the template with global variables
|
||||
if err := templateRenderer.Render(w, file, data); err != nil {
|
||||
|
||||
+197
-65
@@ -2,6 +2,7 @@ package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
||||
commonerrors "github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
@@ -43,20 +45,22 @@ type filteredFileSystem struct {
|
||||
fs webdav.FileSystem
|
||||
source string
|
||||
user *users.User
|
||||
userscope string
|
||||
userScope string
|
||||
// Cache FileInfoFaster results per path to avoid redundant calls within the same request
|
||||
fileInfoCache map[string]*iteminfo.ExtendedFileInfo
|
||||
}
|
||||
|
||||
// getCachedFileInfo retrieves cached FileInfoFaster result, or calls FileInfoFaster and caches it
|
||||
func (ffs *filteredFileSystem) getCachedFileInfo(permissionPath string, expand bool) (*iteminfo.ExtendedFileInfo, error) {
|
||||
// getFileInfo retrieves file information with caching and access control
|
||||
// requestPath should NOT include user scope - FileInfoFaster applies it internally
|
||||
// This function prefers cache, then tries FileInfoFaster which applies access control
|
||||
func (ffs *filteredFileSystem) getFileInfo(requestPath string, expand bool) (*iteminfo.ExtendedFileInfo, error) {
|
||||
// Initialize cache if needed
|
||||
if ffs.fileInfoCache == nil {
|
||||
ffs.fileInfoCache = make(map[string]*iteminfo.ExtendedFileInfo)
|
||||
}
|
||||
|
||||
// Check cache - first try exact match
|
||||
cacheKey := permissionPath
|
||||
// Create cache key (don't modify requestPath itself!)
|
||||
cacheKey := requestPath
|
||||
if expand {
|
||||
cacheKey += ":expand"
|
||||
}
|
||||
@@ -67,34 +71,33 @@ func (ffs *filteredFileSystem) getCachedFileInfo(permissionPath string, expand b
|
||||
// If we're looking for non-expanded but have expanded cached, we can use it
|
||||
// (expanded contains all the same info plus more)
|
||||
if !expand {
|
||||
expandedKey := permissionPath + ":expand"
|
||||
expandedKey := requestPath + ":expand"
|
||||
if cached, found := ffs.fileInfoCache[expandedKey]; found {
|
||||
return cached, nil
|
||||
}
|
||||
}
|
||||
// Call FileInfoFaster
|
||||
|
||||
// Call FileInfoFaster with clean requestPath (without scope or cache suffix)
|
||||
// FileInfoFaster applies user scope internally AND enforces access control
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Path: permissionPath,
|
||||
Path: requestPath,
|
||||
Source: ffs.source,
|
||||
Expand: expand,
|
||||
ShowHidden: ffs.user.ShowHidden,
|
||||
SkipExtendedAttrs: true,
|
||||
FollowSymlinks: true,
|
||||
}, store.Access, ffs.user, store.Share)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache result
|
||||
// Cache result using the cache key
|
||||
ffs.fileInfoCache[cacheKey] = fileInfo
|
||||
|
||||
// If we got an expanded directory listing, also cache individual file entries
|
||||
// so Stat calls on those files can use the cache
|
||||
if expand && fileInfo.Type == "directory" {
|
||||
dirPath := permissionPath
|
||||
if !strings.HasSuffix(dirPath, "/") && dirPath != "" {
|
||||
dirPath += "/"
|
||||
}
|
||||
|
||||
dirPath := requestPath
|
||||
// Cache each file and folder from the directory listing
|
||||
for _, file := range fileInfo.Files {
|
||||
filePath := utils.JoinPathAsUnix(dirPath, file.Name)
|
||||
@@ -121,12 +124,83 @@ func (ffs *filteredFileSystem) getCachedFileInfo(permissionPath string, expand b
|
||||
return fileInfo, nil
|
||||
}
|
||||
|
||||
// checkAccess validates if the user can access a given path
|
||||
// This is used by all write operations (mkdir, delete, rename, etc.)
|
||||
// Returns nil if access is allowed, error otherwise
|
||||
func (ffs *filteredFileSystem) checkAccess(requestPath string) error {
|
||||
// First, validate permissions using CheckPermissions
|
||||
indexPath, _, err := files.CheckPermissions(utils.FileOptions{
|
||||
FollowSymlinks: false,
|
||||
Path: requestPath,
|
||||
Source: ffs.source,
|
||||
ShowHidden: ffs.user.ShowHidden,
|
||||
}, store.Access, ffs.user)
|
||||
if err != nil {
|
||||
logger.Debugf("checkAccess: CheckPermissions denied for %s: %v", requestPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Try to get file info to verify it's accessible
|
||||
// Use non-expanded to be faster (we just need to know if it exists and is accessible)
|
||||
_, err = ffs.getFileInfo(requestPath, false)
|
||||
if err == nil {
|
||||
// Successfully got info - access allowed
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle specific error cases
|
||||
if errors.Is(err, commonerrors.ErrAccessDenied) {
|
||||
logger.Debugf("checkAccess: access explicitly denied for %s", requestPath)
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
// CRITICAL: If item is not indexed AND not viewable, deny access
|
||||
// This prevents WebDAV from creating/modifying files in non-indexed areas
|
||||
if errors.Is(err, commonerrors.ErrNotViewable) {
|
||||
logger.Debugf("checkAccess: path not viewable for %s", requestPath)
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
// If not indexed but potentially viewable, we need to check more carefully
|
||||
if errors.Is(err, commonerrors.ErrNotIndexed) {
|
||||
// Get the index to check viewability
|
||||
idx := indexing.GetIndex(ffs.source)
|
||||
if idx == nil {
|
||||
return fmt.Errorf("source not found")
|
||||
}
|
||||
|
||||
// Check if the item is viewable using GetFileInfo (without expand)
|
||||
info, getErr := idx.GetFileInfo(indexing.FileInfoRequest{
|
||||
IndexPath: indexPath,
|
||||
FollowSymlinks: false,
|
||||
ShowHidden: ffs.user.ShowHidden,
|
||||
Expand: false,
|
||||
SkipExtendedAttrs: true,
|
||||
})
|
||||
|
||||
// If GetFileInfo succeeds, the item is viewable despite not being indexed
|
||||
if getErr == nil && info != nil {
|
||||
logger.Debugf("checkAccess: path not indexed but viewable for %s", requestPath)
|
||||
return nil // Allow access to viewable items
|
||||
}
|
||||
|
||||
// Not viewable - deny access
|
||||
logger.Debugf("checkAccess: path not indexed and not viewable for %s", requestPath)
|
||||
return os.ErrPermission
|
||||
}
|
||||
|
||||
// For other errors (like file not found), that's okay for new file/directory creation
|
||||
// The caller will handle whether the operation is appropriate
|
||||
logger.Debugf("checkAccess: path check returned: %v (may be acceptable for new items)", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// filteredFile wraps a webdav.File and filters Readdir results based on FileInfoFaster
|
||||
type filteredFile struct {
|
||||
webdav.File
|
||||
fs *filteredFileSystem
|
||||
dirPath string // The directory path this file represents (relative to scope root)
|
||||
isDir bool // Whether this is a directory
|
||||
fs *filteredFileSystem
|
||||
requestPath string // The request path (without user scope)
|
||||
isDir bool // Whether this is a directory
|
||||
}
|
||||
|
||||
func (ff *filteredFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
@@ -135,21 +209,23 @@ func (ff *filteredFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
return ff.File.Readdir(count)
|
||||
}
|
||||
|
||||
// For directories, use FileInfoFaster to get filtered contents
|
||||
// Convert dirPath (relative to scope root) to permission path (relative to userscope)
|
||||
// Handle root path specially
|
||||
var permissionPath string
|
||||
if ff.dirPath == "" || ff.dirPath == "/" {
|
||||
permissionPath = ff.fs.userscope
|
||||
} else {
|
||||
permissionPath = utils.JoinPathAsUnix(ff.fs.userscope, ff.dirPath)
|
||||
}
|
||||
|
||||
// Use cached FileInfoFaster result if available
|
||||
fileInfo, err := ff.fs.getCachedFileInfo(permissionPath, true)
|
||||
// Pass the requestPath (without scope) to getCachedFileInfo
|
||||
// FileInfoFaster will apply the user's scope internally
|
||||
fileInfo, err := ff.fs.getFileInfo(ff.requestPath, true)
|
||||
if err != nil {
|
||||
logger.Debugf("readdir: getFileInfo failed for requestPath=%s: %v", ff.requestPath, err)
|
||||
// Handle errors gracefully - return empty directory for access/indexing issues
|
||||
// This is especially important when user's scope points to a non-viewable directory
|
||||
if errors.Is(err, commonerrors.ErrAccessDenied) ||
|
||||
errors.Is(err, commonerrors.ErrNotIndexed) ||
|
||||
errors.Is(err, commonerrors.ErrNotViewable) {
|
||||
logger.Debugf("readdir: access issue for %s: %v - returning empty", ff.requestPath, err)
|
||||
return []os.FileInfo{}, nil
|
||||
}
|
||||
// Other errors - propagate them
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build os.FileInfo list directly from FileInfoFaster's filtered results
|
||||
// No need to read from underlying filesystem - FileInfoFaster already filtered by permissions
|
||||
entries := make([]os.FileInfo, 0, len(fileInfo.Files)+len(fileInfo.Folders))
|
||||
@@ -175,11 +251,34 @@ func (ffs *filteredFileSystem) Mkdir(ctx context.Context, name string, perm os.F
|
||||
if !ffs.user.Permissions.Create {
|
||||
return fmt.Errorf("create permission required")
|
||||
}
|
||||
|
||||
// Check access before creating directory
|
||||
if err := ffs.checkAccess(name); err != nil {
|
||||
logger.Debugf("Mkdir: access denied for %s: %v", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return ffs.fs.Mkdir(ctx, name, perm)
|
||||
}
|
||||
|
||||
func (ffs *filteredFileSystem) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
file, err := ffs.fs.OpenFile(ctx, name, flag, perm)
|
||||
func (ffs *filteredFileSystem) OpenFile(ctx context.Context, requestPath string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
// Check if this is a write operation
|
||||
isWrite := (flag&os.O_WRONLY) != 0 || (flag&os.O_RDWR) != 0 || (flag&os.O_CREATE) != 0
|
||||
|
||||
if isWrite {
|
||||
// Check user permissions first
|
||||
if !ffs.user.Permissions.Create && !ffs.user.Permissions.Modify {
|
||||
return nil, fmt.Errorf("write permission required")
|
||||
}
|
||||
|
||||
// For write operations, check access
|
||||
if err := ffs.checkAccess(requestPath); err != nil {
|
||||
logger.Debugf("OpenFile: write access denied for %s: %v", requestPath, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
file, err := ffs.fs.OpenFile(ctx, requestPath, flag, perm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -191,51 +290,77 @@ func (ffs *filteredFileSystem) OpenFile(ctx context.Context, name string, flag i
|
||||
}
|
||||
|
||||
// Wrap the file to filter directory listings
|
||||
// name is the request path (without user scope)
|
||||
return &filteredFile{
|
||||
File: file,
|
||||
fs: ffs,
|
||||
dirPath: name,
|
||||
isDir: stat.IsDir(),
|
||||
File: file,
|
||||
fs: ffs,
|
||||
requestPath: requestPath,
|
||||
isDir: stat.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ffs *filteredFileSystem) RemoveAll(ctx context.Context, name string) error {
|
||||
func (ffs *filteredFileSystem) RemoveAll(ctx context.Context, requestPath string) error {
|
||||
if !ffs.user.Permissions.Delete {
|
||||
return fmt.Errorf("delete permission required")
|
||||
}
|
||||
return ffs.fs.RemoveAll(ctx, name)
|
||||
|
||||
// Check access before deleting
|
||||
if err := ffs.checkAccess(requestPath); err != nil {
|
||||
logger.Debugf("RemoveAll: access denied for %s: %v", requestPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return ffs.fs.RemoveAll(ctx, requestPath)
|
||||
}
|
||||
|
||||
func (ffs *filteredFileSystem) Rename(ctx context.Context, oldName, newName string) error {
|
||||
func (ffs *filteredFileSystem) Rename(ctx context.Context, oldPath, newPath string) error {
|
||||
if !ffs.user.Permissions.Modify {
|
||||
return fmt.Errorf("modify permission required")
|
||||
}
|
||||
return ffs.fs.Rename(ctx, oldName, newName)
|
||||
|
||||
// Check access for both old and new paths
|
||||
if err := ffs.checkAccess(oldPath); err != nil {
|
||||
logger.Debugf("Rename: access denied for source %s: %v", oldPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ffs.checkAccess(newPath); err != nil {
|
||||
logger.Debugf("Rename: access denied for destination %s: %v", newPath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return ffs.fs.Rename(ctx, oldPath, newPath)
|
||||
}
|
||||
|
||||
func (ffs *filteredFileSystem) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||
// Root path "/" should always be allowed
|
||||
if name == "" || name == "/" {
|
||||
return ffs.fs.Stat(ctx, name)
|
||||
func (ffs *filteredFileSystem) Stat(ctx context.Context, requestPath string) (os.FileInfo, error) {
|
||||
// Try to get file info (uses cache if available, FileInfoFaster otherwise)
|
||||
_, err := ffs.getFileInfo(requestPath, false)
|
||||
if err == nil {
|
||||
// Successfully got info - use underlying filesystem
|
||||
return ffs.fs.Stat(ctx, requestPath)
|
||||
}
|
||||
|
||||
// Check permission before stat using cached result if available
|
||||
permissionPath := utils.JoinPathAsUnix(ffs.userscope, name)
|
||||
_, err := ffs.getCachedFileInfo(permissionPath, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Handle specific error cases
|
||||
if errors.Is(err, commonerrors.ErrAccessDenied) || errors.Is(err, commonerrors.ErrNotViewable) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
return ffs.fs.Stat(ctx, name)
|
||||
// For not indexed, check if it's viewable
|
||||
if errors.Is(err, commonerrors.ErrNotIndexed) {
|
||||
// Use checkAccess to determine if viewable
|
||||
if accessErr := ffs.checkAccess(requestPath); accessErr != nil {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
// Viewable - allow underlying filesystem
|
||||
return ffs.fs.Stat(ctx, requestPath)
|
||||
}
|
||||
|
||||
// For other errors (like file not found), let underlying filesystem handle it
|
||||
return ffs.fs.Stat(ctx, requestPath)
|
||||
}
|
||||
|
||||
// webDAVHandler serves WebDAV requests.
|
||||
func webDAVHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
path := r.PathValue("path")
|
||||
source := r.PathValue("source")
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if !d.user.Permissions.Download {
|
||||
return http.StatusForbidden, fmt.Errorf("download permission required")
|
||||
}
|
||||
@@ -246,15 +371,19 @@ func webDAVHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
if isWrite && !userCanWrite(d.user.Permissions) {
|
||||
return http.StatusForbidden, fmt.Errorf("user has no permission to modify")
|
||||
}
|
||||
logger.Debugf("webdav: method=%s, request=%s, source=%s, path=%s", r.Method, r.URL.Path, source, path)
|
||||
indexPath, userScope, err := files.CheckPermissions(utils.FileOptions{
|
||||
requestPath := utils.AddTrailingSlashIfNotExists(r.PathValue("path"))
|
||||
source := r.PathValue("source")
|
||||
if !strings.HasPrefix(requestPath, "/") {
|
||||
requestPath = "/" + requestPath
|
||||
}
|
||||
logger.Debugf("webdav: method=%s, request=%s, source=%s, requestPath=%s", r.Method, r.URL.Path, source, requestPath)
|
||||
_, userScope, err := files.CheckPermissions(utils.FileOptions{
|
||||
FollowSymlinks: false,
|
||||
Path: path,
|
||||
Path: requestPath,
|
||||
Source: source,
|
||||
ShowHidden: d.user.ShowHidden,
|
||||
}, store.Access, d.user)
|
||||
if err != nil {
|
||||
logger.Debugf("error checking file permissions for path %s: %v", path, err)
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
|
||||
@@ -262,28 +391,25 @@ func webDAVHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
if idx == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source %s not found", source)
|
||||
}
|
||||
|
||||
// Get the user's scope to determine the WebDAV root directory
|
||||
// Resolve the scope path to get the real filesystem root for WebDAV
|
||||
// This is the root directory that WebDAV will use to resolve relative paths
|
||||
scopePath, _, err := idx.GetRealPath(userScope)
|
||||
if err != nil {
|
||||
logger.Debugf("error resolving scope path: %v", err)
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
|
||||
// Construct the WebDAV prefix from BaseURL
|
||||
webDavPrefix := config.Server.BaseURL + "dav"
|
||||
prefix := webDavPrefix + "/" + source
|
||||
logger.Debugf("webdav: virtual_path=%s, indexPath=%s, scope_path=%s", path, indexPath, scopePath)
|
||||
|
||||
// Wrap the filesystem to filter directory listings using FileInfoFaster
|
||||
// This prevents items the user can't access from appearing in listings,
|
||||
// which stops clients from repeatedly trying to access them
|
||||
// We pass requestPath (without scope) to FileInfoFaster, which applies scope internally
|
||||
filteredFS := &filteredFileSystem{
|
||||
fs: webdav.Dir(scopePath),
|
||||
source: source,
|
||||
user: d.user,
|
||||
userscope: userScope,
|
||||
userScope: userScope,
|
||||
}
|
||||
|
||||
wd := &webdav.Handler{
|
||||
@@ -291,7 +417,13 @@ func webDAVHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
FileSystem: filteredFS,
|
||||
LockSystem: idx.WebdavLock,
|
||||
Logger: func(req *http.Request, err error) {
|
||||
if err != nil && !strings.Contains(err.Error(), "no such file or directory") {
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
// Filter out expected/benign errors that don't indicate actual failures
|
||||
if strings.Contains(errStr, "no such file or directory") ||
|
||||
strings.Contains(errStr, "skip this directory") {
|
||||
return
|
||||
}
|
||||
logger.Errorf("webdav handler failed on path %s: %s", req.URL.Path, err)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,803 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
storm "github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
commonerrors "github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
)
|
||||
|
||||
// setupWebDAVTestEnv sets up the test environment with multiple sources and users
|
||||
func setupWebDAVTestEnv(t *testing.T) (string, string) {
|
||||
// Create temp directories for test sources
|
||||
tempDir := t.TempDir()
|
||||
source1Path := filepath.Join(tempDir, "source1")
|
||||
source2Path := filepath.Join(tempDir, "source2")
|
||||
dockerPath := filepath.Join(source1Path, "_docker")
|
||||
|
||||
// Create directory structure
|
||||
dirs := []string{
|
||||
filepath.Join(source1Path, "public"),
|
||||
filepath.Join(source1Path, "private"),
|
||||
filepath.Join(source1Path, "viewable-only"),
|
||||
filepath.Join(source1Path, "not-viewable"),
|
||||
dockerPath,
|
||||
filepath.Join(dockerPath, "data"),
|
||||
filepath.Join(source2Path, "shared"),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create test files
|
||||
testFiles := map[string]string{
|
||||
filepath.Join(source1Path, "public", "readme.txt"): "public content",
|
||||
filepath.Join(source1Path, "private", "secret.txt"): "private content",
|
||||
filepath.Join(source1Path, "viewable-only", "data.txt"): "viewable content",
|
||||
filepath.Join(source1Path, "not-viewable", "hidden.txt"): "hidden content",
|
||||
filepath.Join(dockerPath, "data", "config.yml"): "docker config",
|
||||
filepath.Join(source2Path, "shared", "document.txt"): "shared content",
|
||||
}
|
||||
|
||||
for path, content := range testFiles {
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup database
|
||||
dbPath := filepath.Join(tempDir, "db")
|
||||
db, err := storm.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store, err = bolt.NewStorage(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Clear any previous access rules
|
||||
store.Access.AllRules = make(access.SourceRuleMap)
|
||||
store.Access.Groups = make(access.GroupMap)
|
||||
|
||||
// Set cache directory for index database
|
||||
settings.Config.Server.CacheDir = tempDir
|
||||
|
||||
// Setup config with sources - update both the local config var AND the global settings.Config
|
||||
config = &settings.Settings{
|
||||
Server: settings.Server{
|
||||
BaseURL: "/", // Use / for tests to avoid prefix mismatch
|
||||
CacheDir: tempDir,
|
||||
SourceMap: map[string]*settings.Source{
|
||||
source1Path: {
|
||||
Path: source1Path,
|
||||
Name: "source1",
|
||||
},
|
||||
source2Path: {
|
||||
Path: source2Path,
|
||||
Name: "source2",
|
||||
},
|
||||
},
|
||||
NameToSource: map[string]*settings.Source{
|
||||
"source1": {
|
||||
Path: source1Path,
|
||||
Name: "source1",
|
||||
},
|
||||
"source2": {
|
||||
Path: source2Path,
|
||||
Name: "source2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// CRITICAL: Also update the global settings.Config so access.Permitted can find sources
|
||||
settings.Config.Server.SourceMap = config.Server.SourceMap
|
||||
settings.Config.Server.NameToSource = config.Server.NameToSource
|
||||
|
||||
// Initialize user resolvers
|
||||
settings.InitializeUserResolvers()
|
||||
|
||||
// Create minimal mock indices for webdav handler
|
||||
indexing.SetTestIndex("source1", source1Path)
|
||||
indexing.SetTestIndex("source2", source2Path)
|
||||
t.Cleanup(func() {
|
||||
indexing.ClearTestIndices()
|
||||
})
|
||||
|
||||
// Mock CheckPermissions to bypass index requirement
|
||||
mockCheckPermissions(t, source1Path, source2Path)
|
||||
|
||||
// Initialize mock indexing - this mocks FileInfoFaster
|
||||
mockWebDAVIndexing(t, source1Path, source2Path)
|
||||
|
||||
return source1Path, source2Path
|
||||
}
|
||||
|
||||
// mockCheckPermissions mocks the CheckPermissions function to bypass index lookups
|
||||
func mockCheckPermissions(t *testing.T, source1Path, source2Path string) {
|
||||
t.Helper()
|
||||
|
||||
// Store original function
|
||||
originalCheckPermissions := files.CheckPermissionsFunc
|
||||
t.Cleanup(func() { files.CheckPermissionsFunc = originalCheckPermissions })
|
||||
|
||||
// Mock the function
|
||||
files.CheckPermissionsFunc = func(opts utils.FileOptions, access *access.Storage, user *users.User) (string, string, error) {
|
||||
// Get the source path
|
||||
var sourcePath string
|
||||
if opts.Source == "source1" {
|
||||
sourcePath = source1Path
|
||||
} else if opts.Source == "source2" {
|
||||
sourcePath = source2Path
|
||||
} else {
|
||||
return "", "", fmt.Errorf("unknown source: %s", opts.Source)
|
||||
}
|
||||
|
||||
// Get user scope for this source - match by source PATH, not name
|
||||
userScope := "/"
|
||||
hasScope := false
|
||||
for _, scope := range user.Scopes {
|
||||
if scope.Name == sourcePath {
|
||||
userScope = scope.Scope
|
||||
hasScope = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If user has no scope for this source, deny access
|
||||
if !hasScope && len(user.Scopes) > 0 {
|
||||
return "", "", fmt.Errorf("user has no access to source: %s", opts.Source)
|
||||
}
|
||||
|
||||
// Sanitize path
|
||||
safePath, err := utils.SanitizeUserPath(opts.Path)
|
||||
if err != nil {
|
||||
return "", "", commonerrors.ErrAccessDenied
|
||||
}
|
||||
|
||||
// Combine scope + sanitized path
|
||||
indexPath := utils.JoinPathAsUnix(userScope, safePath)
|
||||
|
||||
// Check access control
|
||||
if access != nil && !access.Permitted(sourcePath, indexPath, user.Username) {
|
||||
return "", "", commonerrors.ErrAccessDenied
|
||||
}
|
||||
|
||||
return indexPath, userScope, nil
|
||||
}
|
||||
}
|
||||
|
||||
// mockWebDAVIndexing mocks the indexing system for WebDAV tests
|
||||
func mockWebDAVIndexing(t *testing.T, source1Path, source2Path string) {
|
||||
// Mock FileInfoFaster to simulate indexing behavior
|
||||
originalFileInfoFaster := files.FileInfoFasterFunc
|
||||
t.Cleanup(func() { files.FileInfoFasterFunc = originalFileInfoFaster })
|
||||
|
||||
files.FileInfoFasterFunc = func(opts utils.FileOptions, access *access.Storage, user *users.User, share *share.Storage) (*iteminfo.ExtendedFileInfo, error) {
|
||||
// Resolve source name to source path first
|
||||
sourcePath := ""
|
||||
if opts.Source == "source1" {
|
||||
sourcePath = source1Path
|
||||
} else if opts.Source == "source2" {
|
||||
sourcePath = source2Path
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown source: %s", opts.Source)
|
||||
}
|
||||
|
||||
// Simulate access control
|
||||
if access != nil && user != nil {
|
||||
// Simple scope check based on user's configured scopes (which use source PATH)
|
||||
hasAccess := false
|
||||
for _, scope := range user.Scopes {
|
||||
if scope.Name == sourcePath {
|
||||
hasAccess = true
|
||||
// Check if path is within user's scope
|
||||
userScope := scope.Scope
|
||||
fullPath := utils.JoinPathAsUnix(userScope, opts.Path)
|
||||
|
||||
// Check if user has permission using the access storage
|
||||
if !access.Permitted(sourcePath, fullPath, user.Username) {
|
||||
return nil, commonerrors.ErrAccessDenied
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAccess && len(user.Scopes) > 0 {
|
||||
return nil, commonerrors.ErrAccessDenied
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate indexing rules
|
||||
path := opts.Path
|
||||
|
||||
// Paths containing "not-viewable" are not indexed and not viewable
|
||||
if strings.Contains(path, "not-viewable") {
|
||||
return nil, commonerrors.ErrNotViewable
|
||||
}
|
||||
|
||||
// Paths containing "viewable-only" are not indexed but viewable
|
||||
if strings.Contains(path, "viewable-only") {
|
||||
return nil, commonerrors.ErrNotIndexed
|
||||
}
|
||||
|
||||
// Simulate directory expansion
|
||||
var files []iteminfo.ExtendedItemInfo
|
||||
var folders []iteminfo.ItemInfo
|
||||
|
||||
if opts.Expand && (path == "/" || strings.HasSuffix(path, "/")) {
|
||||
// Return mock directory contents based on path
|
||||
if path == "/" || path == "" {
|
||||
folders = []iteminfo.ItemInfo{
|
||||
{Name: "public", Type: "directory"},
|
||||
{Name: "private", Type: "directory"},
|
||||
{Name: "viewable-only", Type: "directory"},
|
||||
}
|
||||
} else if strings.Contains(path, "public") {
|
||||
files = []iteminfo.ExtendedItemInfo{
|
||||
{ItemInfo: iteminfo.ItemInfo{Name: "readme.txt", Size: 14, Type: "text/plain"}},
|
||||
}
|
||||
} else if strings.Contains(path, "private") {
|
||||
files = []iteminfo.ExtendedItemInfo{
|
||||
{ItemInfo: iteminfo.ItemInfo{Name: "secret.txt", Size: 15, Type: "text/plain"}},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &iteminfo.ExtendedFileInfo{
|
||||
FileInfo: iteminfo.FileInfo{
|
||||
Path: path,
|
||||
Files: files,
|
||||
Folders: folders,
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Name: filepath.Base(path),
|
||||
Type: "directory",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Test PROPFIND with different user scopes
|
||||
func TestWebDAV_PROPFIND_UserScopes(t *testing.T) {
|
||||
source1Path, source2Path := setupWebDAVTestEnv(t)
|
||||
|
||||
// Create users with different scopes
|
||||
// IMPORTANT: SourceScope.Name should be the source PATH, not the source name
|
||||
adminUser := &users.User{
|
||||
ID: 1,
|
||||
Username: "admin",
|
||||
Permissions: users.Permissions{
|
||||
Admin: true,
|
||||
Download: true,
|
||||
Create: true,
|
||||
Delete: true,
|
||||
Modify: true,
|
||||
},
|
||||
Scopes: []users.SourceScope{
|
||||
{Name: source1Path, Scope: "/"},
|
||||
{Name: source2Path, Scope: "/"},
|
||||
},
|
||||
}
|
||||
|
||||
scopedUser := &users.User{
|
||||
ID: 2,
|
||||
Username: "scoped",
|
||||
Permissions: users.Permissions{
|
||||
Download: true,
|
||||
},
|
||||
Scopes: []users.SourceScope{
|
||||
{Name: source1Path, Scope: "/_docker"},
|
||||
},
|
||||
}
|
||||
|
||||
restrictedUser := &users.User{
|
||||
ID: 3,
|
||||
Username: "restricted",
|
||||
Permissions: users.Permissions{
|
||||
Download: true,
|
||||
},
|
||||
Scopes: []users.SourceScope{
|
||||
{Name: source1Path, Scope: "/public"},
|
||||
},
|
||||
}
|
||||
|
||||
// Save users
|
||||
if err := store.Users.Save(adminUser, true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Users.Save(scopedUser, true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Users.Save(restrictedUser, true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *users.User
|
||||
source string
|
||||
path string
|
||||
expectedStatus int
|
||||
shouldHaveContent bool
|
||||
}{
|
||||
{
|
||||
name: "Admin can access root of source1",
|
||||
user: adminUser,
|
||||
source: "source1",
|
||||
path: "/",
|
||||
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
|
||||
shouldHaveContent: true,
|
||||
},
|
||||
{
|
||||
name: "Scoped user can access their scope root",
|
||||
user: scopedUser,
|
||||
source: "source1",
|
||||
path: "/",
|
||||
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
|
||||
shouldHaveContent: false, // Empty because _docker is not viewable
|
||||
},
|
||||
{
|
||||
name: "Restricted user can only see public folder",
|
||||
user: restrictedUser,
|
||||
source: "source1",
|
||||
path: "/",
|
||||
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
|
||||
shouldHaveContent: true,
|
||||
},
|
||||
{
|
||||
name: "Admin can access source2",
|
||||
user: adminUser,
|
||||
source: "source2",
|
||||
path: "/",
|
||||
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
|
||||
shouldHaveContent: true,
|
||||
},
|
||||
{
|
||||
name: "Scoped user cannot access source2",
|
||||
user: scopedUser,
|
||||
source: "source2",
|
||||
path: "/",
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize indexing for sources
|
||||
initTestIndex(t, "source1", source1Path)
|
||||
initTestIndex(t, "source2", source2Path)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("PROPFIND", "/dav/"+tc.source+tc.path, nil)
|
||||
req.SetPathValue("source", tc.source)
|
||||
req.SetPathValue("path", tc.path)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx := &requestContext{user: tc.user}
|
||||
|
||||
status, err := webDAVHandler(w, req, ctx)
|
||||
|
||||
// If handler returned an error status, use that instead of response code
|
||||
if status != 0 && status != http.StatusOK {
|
||||
if w.Code == 0 || w.Code == http.StatusOK {
|
||||
w.Code = status
|
||||
}
|
||||
}
|
||||
|
||||
// Check the actual HTTP response status code from ResponseWriter
|
||||
if w.Code != tc.expectedStatus {
|
||||
t.Errorf("expected status %d, got %d (err: %v)", tc.expectedStatus, w.Code, err)
|
||||
}
|
||||
|
||||
if tc.shouldHaveContent && w.Code == 207 {
|
||||
body := w.Body.String()
|
||||
if body == "" {
|
||||
t.Error("expected response body to have content")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test WebDAV write operations with access control
|
||||
func TestWebDAV_WriteOperations(t *testing.T) {
|
||||
source1Path, _ := setupWebDAVTestEnv(t)
|
||||
|
||||
// Create users with different permissions
|
||||
// IMPORTANT: SourceScope.Name should be the source PATH, not the source name
|
||||
fullAccessUser := &users.User{
|
||||
ID: 1,
|
||||
Username: "fullaccess",
|
||||
Permissions: users.Permissions{
|
||||
Download: true,
|
||||
Create: true,
|
||||
Delete: true,
|
||||
Modify: true,
|
||||
},
|
||||
Scopes: []users.SourceScope{
|
||||
{Name: source1Path, Scope: "/"},
|
||||
},
|
||||
}
|
||||
|
||||
readOnlyUser := &users.User{
|
||||
ID: 2,
|
||||
Username: "readonly",
|
||||
Permissions: users.Permissions{
|
||||
Download: true,
|
||||
},
|
||||
Scopes: []users.SourceScope{
|
||||
{Name: source1Path, Scope: "/"},
|
||||
},
|
||||
}
|
||||
|
||||
scopedUser := &users.User{
|
||||
ID: 3,
|
||||
Username: "scoped",
|
||||
Permissions: users.Permissions{
|
||||
Download: true,
|
||||
Create: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
},
|
||||
Scopes: []users.SourceScope{
|
||||
{Name: source1Path, Scope: "/public"},
|
||||
},
|
||||
}
|
||||
|
||||
// Save users
|
||||
if err := store.Users.Save(fullAccessUser, true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Users.Save(readOnlyUser, true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Users.Save(scopedUser, true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
initTestIndex(t, "source1", source1Path)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *users.User
|
||||
method string
|
||||
path string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Full access user can PUT file",
|
||||
user: fullAccessUser,
|
||||
method: http.MethodPut,
|
||||
path: "/public/newfile.txt",
|
||||
expectedStatus: http.StatusCreated, // 201 for new file creation
|
||||
},
|
||||
{
|
||||
name: "Read-only user cannot PUT file",
|
||||
user: readOnlyUser,
|
||||
method: http.MethodPut,
|
||||
path: "/public/newfile.txt",
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "Full access user can MKCOL",
|
||||
user: fullAccessUser,
|
||||
method: "MKCOL",
|
||||
path: "/newdir",
|
||||
expectedStatus: http.StatusCreated, // 201 for new directory
|
||||
},
|
||||
{
|
||||
name: "Read-only user cannot MKCOL",
|
||||
user: readOnlyUser,
|
||||
method: "MKCOL",
|
||||
path: "/newdir",
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "Full access user can DELETE",
|
||||
user: fullAccessUser,
|
||||
method: http.MethodDelete,
|
||||
path: "/public/readme.txt",
|
||||
expectedStatus: http.StatusNoContent, // 204 for successful deletion
|
||||
},
|
||||
{
|
||||
name: "Read-only user cannot DELETE",
|
||||
user: readOnlyUser,
|
||||
method: http.MethodDelete,
|
||||
path: "/public/readme.txt",
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "Scoped user can write in their scope",
|
||||
user: scopedUser,
|
||||
method: http.MethodPut,
|
||||
path: "/scopedfile.txt", // Path relative to scope (/public)
|
||||
expectedStatus: http.StatusCreated, // 201 for new file
|
||||
},
|
||||
{
|
||||
name: "User cannot write to not-viewable directory",
|
||||
user: fullAccessUser,
|
||||
method: http.MethodPut,
|
||||
path: "/not-viewable/file.txt",
|
||||
expectedStatus: 404, // Not viewable = acts like it doesn't exist
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var body io.Reader
|
||||
if tc.method == http.MethodPut {
|
||||
body = strings.NewReader("test content")
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(tc.method, "/dav/source1"+tc.path, body)
|
||||
req.SetPathValue("source", "source1")
|
||||
req.SetPathValue("path", tc.path)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx := &requestContext{user: tc.user}
|
||||
|
||||
status, err := webDAVHandler(w, req, ctx)
|
||||
|
||||
// If handler returned an error status, use that instead of response code
|
||||
if status != 0 && status != http.StatusOK {
|
||||
if w.Code == 0 || w.Code == http.StatusOK {
|
||||
w.Code = status
|
||||
}
|
||||
}
|
||||
|
||||
// Check the actual HTTP response status code from ResponseWriter
|
||||
if w.Code != tc.expectedStatus {
|
||||
t.Errorf("expected status %d, got %d (err: %v)", tc.expectedStatus, w.Code, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test access control rules
|
||||
func TestWebDAV_AccessControl(t *testing.T) {
|
||||
source1Path, _ := setupWebDAVTestEnv(t)
|
||||
|
||||
// Create users
|
||||
// IMPORTANT: SourceScope.Name should be the source PATH, not the source name
|
||||
user1 := &users.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Permissions: users.Permissions{
|
||||
Download: true,
|
||||
Create: true,
|
||||
},
|
||||
Scopes: []users.SourceScope{
|
||||
{Name: source1Path, Scope: "/"},
|
||||
},
|
||||
}
|
||||
|
||||
user2 := &users.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Permissions: users.Permissions{
|
||||
Download: true,
|
||||
},
|
||||
Scopes: []users.SourceScope{
|
||||
{Name: source1Path, Scope: "/"},
|
||||
},
|
||||
}
|
||||
|
||||
// Save users
|
||||
if err := store.Users.Save(user1, true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Users.Save(user2, true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Initialize index
|
||||
initTestIndex(t, "source1", source1Path)
|
||||
|
||||
// Create access rules by using the internal structure
|
||||
// User1 can access /private, user2 cannot
|
||||
// Set up a deny rule for user2 at /private
|
||||
// IMPORTANT: Access system requires trailing slashes for exact match
|
||||
rule := &access.AccessRule{
|
||||
Deny: access.RuleSet{
|
||||
Users: access.StringSet{"user2": struct{}{}},
|
||||
},
|
||||
}
|
||||
|
||||
// Add the rule to the access storage
|
||||
if store.Access.AllRules == nil {
|
||||
store.Access.AllRules = make(access.SourceRuleMap)
|
||||
}
|
||||
if store.Access.AllRules[source1Path] == nil {
|
||||
store.Access.AllRules[source1Path] = make(access.RuleMap)
|
||||
}
|
||||
store.Access.AllRules[source1Path]["/private/"] = rule // Add trailing slash
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *users.User
|
||||
path string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "User1 can access private folder",
|
||||
user: user1,
|
||||
path: "/private",
|
||||
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
|
||||
},
|
||||
{
|
||||
name: "User2 cannot access private folder",
|
||||
user: user2,
|
||||
path: "/private",
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "Both users can access public folder",
|
||||
user: user1,
|
||||
path: "/public",
|
||||
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
|
||||
},
|
||||
{
|
||||
name: "User2 can also access public folder",
|
||||
user: user2,
|
||||
path: "/public",
|
||||
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("PROPFIND", "/dav/source1"+tc.path, nil)
|
||||
req.SetPathValue("source", "source1")
|
||||
req.SetPathValue("path", tc.path)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx := &requestContext{user: tc.user}
|
||||
|
||||
status, err := webDAVHandler(w, req, ctx)
|
||||
|
||||
// If handler returned an error status, use that instead of response code
|
||||
if status != 0 && status != http.StatusOK {
|
||||
if w.Code == 0 || w.Code == http.StatusOK {
|
||||
w.Code = status
|
||||
}
|
||||
}
|
||||
|
||||
// Check the actual HTTP response status code from ResponseWriter
|
||||
if w.Code != tc.expectedStatus {
|
||||
t.Errorf("expected status %d, got %d (err: %v)", tc.expectedStatus, w.Code, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test indexing states (indexed, viewable, not-viewable)
|
||||
func TestWebDAV_IndexingStates(t *testing.T) {
|
||||
source1Path, _ := setupWebDAVTestEnv(t)
|
||||
|
||||
// IMPORTANT: SourceScope.Name should be the source PATH, not the source name
|
||||
user := &users.User{
|
||||
ID: 1,
|
||||
Username: "testuser",
|
||||
Permissions: users.Permissions{
|
||||
Download: true,
|
||||
Create: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
},
|
||||
Scopes: []users.SourceScope{
|
||||
{Name: source1Path, Scope: "/"},
|
||||
},
|
||||
}
|
||||
|
||||
if err := store.Users.Save(user, true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
initTestIndex(t, "source1", source1Path)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
method string
|
||||
expectedStatus int
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Can read indexed directory",
|
||||
path: "/public",
|
||||
method: "PROPFIND",
|
||||
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
|
||||
description: "Indexed directories should be accessible",
|
||||
},
|
||||
{
|
||||
name: "Can read viewable-only directory",
|
||||
path: "/viewable-only",
|
||||
method: "PROPFIND",
|
||||
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
|
||||
description: "Not indexed but viewable directories should be readable",
|
||||
},
|
||||
{
|
||||
name: "Cannot read not-viewable directory",
|
||||
path: "/not-viewable",
|
||||
method: "PROPFIND",
|
||||
expectedStatus: 405, // WebDAV returns 405 Method Not Allowed when resource denied
|
||||
description: "Not viewable directories should be denied",
|
||||
},
|
||||
{
|
||||
name: "Cannot write to viewable-only directory",
|
||||
path: "/viewable-only/newfile.txt",
|
||||
method: http.MethodPut,
|
||||
expectedStatus: 404, // Returns 404 when trying to write to non-existent file in denied location
|
||||
description: "Viewable-only directories should deny write",
|
||||
},
|
||||
{
|
||||
name: "Cannot write to not-viewable directory",
|
||||
path: "/not-viewable/newfile.txt",
|
||||
method: http.MethodPut,
|
||||
expectedStatus: 404, // Not viewable = acts like it doesn't exist
|
||||
description: "Not viewable directories should deny all access",
|
||||
},
|
||||
{
|
||||
name: "Can write to indexed directory",
|
||||
path: "/public/newfile.txt",
|
||||
method: http.MethodPut,
|
||||
expectedStatus: http.StatusCreated, // 201 for new file creation
|
||||
description: "Indexed directories should allow write with permissions",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var body io.Reader
|
||||
if tc.method == http.MethodPut {
|
||||
body = strings.NewReader("test content")
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(tc.method, "/dav/source1"+tc.path, body)
|
||||
req.SetPathValue("source", "source1")
|
||||
req.SetPathValue("path", tc.path)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ctx := &requestContext{user: user}
|
||||
|
||||
status, err := webDAVHandler(w, req, ctx)
|
||||
|
||||
// If handler returned an error status, use that instead of response code
|
||||
if status != 0 && status != http.StatusOK {
|
||||
if w.Code == 0 || w.Code == http.StatusOK {
|
||||
w.Code = status
|
||||
}
|
||||
}
|
||||
|
||||
// Check the actual HTTP response status code from ResponseWriter
|
||||
if w.Code != tc.expectedStatus {
|
||||
t.Errorf("%s: expected status %d, got %d (err: %v)", tc.description, tc.expectedStatus, w.Code, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to initialize a test index - simplified for WebDAV tests
|
||||
func initTestIndex(t *testing.T, name, path string) {
|
||||
// For WebDAV tests, indices are already initialized in setupWebDAVTestEnv
|
||||
t.Helper()
|
||||
}
|
||||
@@ -508,7 +508,7 @@ func (idx *Index) GetFileInfo(req FileInfoRequest) (*iteminfo.FileInfo, error) {
|
||||
|
||||
// Path must be either viewable OR indexable
|
||||
if !isViewable && !isIndexable {
|
||||
return nil, errors.ErrNotIndexed
|
||||
return nil, errors.ErrNotViewable
|
||||
}
|
||||
|
||||
// 3. Return appropriate info
|
||||
@@ -659,10 +659,10 @@ func (idx *Index) GetFsInfoCore(indexPath string, opts Options) (*iteminfo.FileI
|
||||
// Deny access if not viewable AND skipped
|
||||
if opts.CheckViewable {
|
||||
if !isViewable {
|
||||
return nil, errors.ErrNotIndexed
|
||||
return nil, errors.ErrNotViewable
|
||||
}
|
||||
} else if isSkipped {
|
||||
return nil, errors.ErrNotIndexed
|
||||
return nil, errors.ErrNotViewable
|
||||
}
|
||||
|
||||
realSize, _ := idx.handleFile(dirInfo, indexPath, realPath, false, nil)
|
||||
|
||||
@@ -100,29 +100,15 @@ func (info *FileInfo) SortItems() {
|
||||
}
|
||||
|
||||
// ResolveSymlinks resolves symlinks in the given path and returns
|
||||
// the final resolved path, whether it's a directory (considering bundle logic), and any error.
|
||||
// Uses Go's filepath.EvalSymlinks which properly detects circular symlinks.
|
||||
func ResolveSymlinks(path string) (string, bool, error) {
|
||||
for {
|
||||
// Get the file info using os.Lstat to handle symlinks
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return path, false, fmt.Errorf("could not stat path: %s, %v", path, err)
|
||||
}
|
||||
|
||||
// Check if the path is a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
// Read the symlink target
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return path, false, fmt.Errorf("could not read symlink: %s, %v", path, err)
|
||||
}
|
||||
|
||||
// Resolve the symlink's target relative to its directory
|
||||
path = filepath.Join(filepath.Dir(path), target)
|
||||
} else {
|
||||
// Not a symlink, check with bundle-aware directory logic
|
||||
isDir := IsDirectory(info)
|
||||
return path, isDir, nil
|
||||
}
|
||||
resolvedPath, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return path, false, fmt.Errorf("could not resolve symlinks for %s: %v", path, err)
|
||||
}
|
||||
info, err := os.Lstat(resolvedPath)
|
||||
if err != nil {
|
||||
return resolvedPath, false, fmt.Errorf("could not stat resolved path %s: %v", resolvedPath, err)
|
||||
}
|
||||
return resolvedPath, IsDirectory(info), nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package indexing
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
// SetTestIndex allows tests to register mock indices without database initialization
|
||||
// This is useful for testing code that depends on GetIndex() without needing full setup
|
||||
func SetTestIndex(name string, path string) {
|
||||
indexesMutex.Lock()
|
||||
defer indexesMutex.Unlock()
|
||||
|
||||
idx := &Index{
|
||||
Source: settings.Source{
|
||||
Name: name,
|
||||
Path: path,
|
||||
},
|
||||
WebdavLock: webdav.NewMemLS(),
|
||||
mock: true,
|
||||
}
|
||||
indexes[name] = idx
|
||||
}
|
||||
|
||||
// ClearTestIndices removes all test indices - call in test cleanup
|
||||
func ClearTestIndices() {
|
||||
indexesMutex.Lock()
|
||||
defer indexesMutex.Unlock()
|
||||
indexes = make(map[string]*Index)
|
||||
}
|
||||
@@ -4102,6 +4102,9 @@ const docTemplate = `{
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEditShare": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4213,6 +4216,9 @@ const docTemplate = `{
|
||||
"description": "Override embedded field to show source name",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5463,6 +5469,10 @@ const docTemplate = `{
|
||||
"description": "debug onlyoffice editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteAfterArchive": {
|
||||
"description": "delete source files after successful creation/extraction of archives",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteWithoutConfirming": {
|
||||
"description": "delete files without confirmation",
|
||||
"type": "boolean"
|
||||
@@ -5515,6 +5525,10 @@ const docTemplate = `{
|
||||
"description": "0-9 - the size of the gallery thumbnails",
|
||||
"type": "integer"
|
||||
},
|
||||
"hideFilesInTree": {
|
||||
"description": "hide files in the sidebar tree navigation, when true, will show only directories.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hideSidebarFileActions": {
|
||||
"description": "hide the file actions in the sidebar",
|
||||
"type": "boolean"
|
||||
@@ -5607,6 +5621,10 @@ const docTemplate = `{
|
||||
"settings.UserDefaultsPreview": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"audio": {
|
||||
"description": "show thumbnails for audio files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"autoplayMedia": {
|
||||
"description": "autoplay media files in preview",
|
||||
"type": "boolean"
|
||||
@@ -5631,6 +5649,10 @@ const docTemplate = `{
|
||||
"description": "show thumbnails for image files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"models": {
|
||||
"description": "show live thumbnails for 3D models files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"motionVideoPreview": {
|
||||
"description": "show multiple frames for videos in thumbnail preview when hovering",
|
||||
"type": "boolean"
|
||||
@@ -5679,6 +5701,9 @@ const docTemplate = `{
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEditShare": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5775,6 +5800,9 @@ const docTemplate = `{
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5817,6 +5845,9 @@ const docTemplate = `{
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEditShare": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5922,6 +5953,9 @@ const docTemplate = `{
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5967,6 +6001,9 @@ const docTemplate = `{
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEditShare": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6075,6 +6112,9 @@ const docTemplate = `{
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6205,6 +6245,10 @@ const docTemplate = `{
|
||||
"users.Preview": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"audio": {
|
||||
"description": "show thumbnail preview image for audio files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"autoplayMedia": {
|
||||
"description": "autoplay media files in preview",
|
||||
"type": "boolean"
|
||||
@@ -6225,6 +6269,10 @@ const docTemplate = `{
|
||||
"description": "show thumbnail preview image for image files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"models": {
|
||||
"description": "show live thumbnails for 3D models files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"motionVideoPreview": {
|
||||
"description": "show multiple frames for videos in thumbnail preview when hovering",
|
||||
"type": "boolean"
|
||||
@@ -6316,6 +6364,10 @@ const docTemplate = `{
|
||||
"description": "debug onlyoffice editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteAfterArchive": {
|
||||
"description": "delete source files after successful creation/extraction of archives",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteWithoutConfirming": {
|
||||
"description": "delete files without confirmation",
|
||||
"type": "boolean"
|
||||
@@ -6367,6 +6419,10 @@ const docTemplate = `{
|
||||
"description": "0-9 - the size of the gallery thumbnails",
|
||||
"type": "integer"
|
||||
},
|
||||
"hideFilesInTree": {
|
||||
"description": "hide files in the sidebar tree navigation, when true, will show only directories.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hideSidebarFileActions": {
|
||||
"description": "hide the file actions in the sidebar",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -4091,6 +4091,9 @@
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEditShare": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4202,6 +4205,9 @@
|
||||
"description": "Override embedded field to show source name",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5452,6 +5458,10 @@
|
||||
"description": "debug onlyoffice editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteAfterArchive": {
|
||||
"description": "delete source files after successful creation/extraction of archives",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteWithoutConfirming": {
|
||||
"description": "delete files without confirmation",
|
||||
"type": "boolean"
|
||||
@@ -5504,6 +5514,10 @@
|
||||
"description": "0-9 - the size of the gallery thumbnails",
|
||||
"type": "integer"
|
||||
},
|
||||
"hideFilesInTree": {
|
||||
"description": "hide files in the sidebar tree navigation, when true, will show only directories.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hideSidebarFileActions": {
|
||||
"description": "hide the file actions in the sidebar",
|
||||
"type": "boolean"
|
||||
@@ -5596,6 +5610,10 @@
|
||||
"settings.UserDefaultsPreview": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"audio": {
|
||||
"description": "show thumbnails for audio files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"autoplayMedia": {
|
||||
"description": "autoplay media files in preview",
|
||||
"type": "boolean"
|
||||
@@ -5620,6 +5638,10 @@
|
||||
"description": "show thumbnails for image files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"models": {
|
||||
"description": "show live thumbnails for 3D models files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"motionVideoPreview": {
|
||||
"description": "show multiple frames for videos in thumbnail preview when hovering",
|
||||
"type": "boolean"
|
||||
@@ -5668,6 +5690,9 @@
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEditShare": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5764,6 +5789,9 @@
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5806,6 +5834,9 @@
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEditShare": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5911,6 +5942,9 @@
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5956,6 +5990,9 @@
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEditShare": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6064,6 +6101,9 @@
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -6194,6 +6234,10 @@
|
||||
"users.Preview": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"audio": {
|
||||
"description": "show thumbnail preview image for audio files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"autoplayMedia": {
|
||||
"description": "autoplay media files in preview",
|
||||
"type": "boolean"
|
||||
@@ -6214,6 +6258,10 @@
|
||||
"description": "show thumbnail preview image for image files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"models": {
|
||||
"description": "show live thumbnails for 3D models files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"motionVideoPreview": {
|
||||
"description": "show multiple frames for videos in thumbnail preview when hovering",
|
||||
"type": "boolean"
|
||||
@@ -6305,6 +6353,10 @@
|
||||
"description": "debug onlyoffice editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteAfterArchive": {
|
||||
"description": "delete source files after successful creation/extraction of archives",
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteWithoutConfirming": {
|
||||
"description": "delete files without confirmation",
|
||||
"type": "boolean"
|
||||
@@ -6356,6 +6408,10 @@
|
||||
"description": "0-9 - the size of the gallery thumbnails",
|
||||
"type": "integer"
|
||||
},
|
||||
"hideFilesInTree": {
|
||||
"description": "hide files in the sidebar tree navigation, when true, will show only directories.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hideSidebarFileActions": {
|
||||
"description": "hide the file actions in the sidebar",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -131,6 +131,8 @@ definitions:
|
||||
type: string
|
||||
bannerUrl:
|
||||
type: string
|
||||
canEditShare:
|
||||
type: boolean
|
||||
description:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
@@ -208,6 +210,8 @@ definitions:
|
||||
source:
|
||||
description: Override embedded field to show source name
|
||||
type: string
|
||||
sourceURL:
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
title:
|
||||
@@ -1177,6 +1181,9 @@ definitions:
|
||||
debugOffice:
|
||||
description: debug onlyoffice editor
|
||||
type: boolean
|
||||
deleteAfterArchive:
|
||||
description: delete source files after successful creation/extraction of archives
|
||||
type: boolean
|
||||
deleteWithoutConfirming:
|
||||
description: delete files without confirmation
|
||||
type: boolean
|
||||
@@ -1214,6 +1221,10 @@ definitions:
|
||||
gallerySize:
|
||||
description: 0-9 - the size of the gallery thumbnails
|
||||
type: integer
|
||||
hideFilesInTree:
|
||||
description: hide files in the sidebar tree navigation, when true, will show
|
||||
only directories.
|
||||
type: boolean
|
||||
hideSidebarFileActions:
|
||||
description: hide the file actions in the sidebar
|
||||
type: boolean
|
||||
@@ -1284,6 +1295,9 @@ definitions:
|
||||
type: object
|
||||
settings.UserDefaultsPreview:
|
||||
properties:
|
||||
audio:
|
||||
description: show thumbnails for audio files
|
||||
type: boolean
|
||||
autoplayMedia:
|
||||
description: autoplay media files in preview
|
||||
type: boolean
|
||||
@@ -1302,6 +1316,9 @@ definitions:
|
||||
image:
|
||||
description: show thumbnails for image files
|
||||
type: boolean
|
||||
models:
|
||||
description: show live thumbnails for 3D models files
|
||||
type: boolean
|
||||
motionVideoPreview:
|
||||
description: show multiple frames for videos in thumbnail preview when hovering
|
||||
type: boolean
|
||||
@@ -1336,6 +1353,8 @@ definitions:
|
||||
type: string
|
||||
bannerUrl:
|
||||
type: string
|
||||
canEditShare:
|
||||
type: boolean
|
||||
description:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
@@ -1403,6 +1422,8 @@ definitions:
|
||||
source:
|
||||
description: backend source is path to maintain between name changes
|
||||
type: string
|
||||
sourceURL:
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
title:
|
||||
@@ -1433,6 +1454,8 @@ definitions:
|
||||
type: string
|
||||
bannerUrl:
|
||||
type: string
|
||||
canEditShare:
|
||||
type: boolean
|
||||
description:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
@@ -1506,6 +1529,8 @@ definitions:
|
||||
source:
|
||||
description: backend source is path to maintain between name changes
|
||||
type: string
|
||||
sourceURL:
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
title:
|
||||
@@ -1538,6 +1563,8 @@ definitions:
|
||||
type: string
|
||||
bannerUrl:
|
||||
type: string
|
||||
canEditShare:
|
||||
type: boolean
|
||||
description:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
@@ -1613,6 +1640,8 @@ definitions:
|
||||
source:
|
||||
description: backend source is path to maintain between name changes
|
||||
type: string
|
||||
sourceURL:
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
title:
|
||||
@@ -1709,6 +1738,9 @@ definitions:
|
||||
type: object
|
||||
users.Preview:
|
||||
properties:
|
||||
audio:
|
||||
description: show thumbnail preview image for audio files
|
||||
type: boolean
|
||||
autoplayMedia:
|
||||
description: autoplay media files in preview
|
||||
type: boolean
|
||||
@@ -1724,6 +1756,9 @@ definitions:
|
||||
image:
|
||||
description: show thumbnail preview image for image files
|
||||
type: boolean
|
||||
models:
|
||||
description: show live thumbnails for 3D models files
|
||||
type: boolean
|
||||
motionVideoPreview:
|
||||
description: show multiple frames for videos in thumbnail preview when hovering
|
||||
type: boolean
|
||||
@@ -1790,6 +1825,9 @@ definitions:
|
||||
debugOffice:
|
||||
description: debug onlyoffice editor
|
||||
type: boolean
|
||||
deleteAfterArchive:
|
||||
description: delete source files after successful creation/extraction of archives
|
||||
type: boolean
|
||||
deleteWithoutConfirming:
|
||||
description: delete files without confirmation
|
||||
type: boolean
|
||||
@@ -1826,6 +1864,10 @@ definitions:
|
||||
gallerySize:
|
||||
description: 0-9 - the size of the gallery thumbnails
|
||||
type: integer
|
||||
hideFilesInTree:
|
||||
description: hide files in the sidebar tree navigation, when true, will show
|
||||
only directories.
|
||||
type: boolean
|
||||
hideSidebarFileActions:
|
||||
description: hide the file actions in the sidebar
|
||||
type: boolean
|
||||
|
||||
Generated
+128
-1077
File diff suppressed because it is too large.
Load diff
@@ -27,7 +27,6 @@
|
||||
"css-vars-ponyfill": "^2.4.3",
|
||||
"dompurify": "^3.2.4",
|
||||
"epubjs": "^0.3.93",
|
||||
"file-loader": "^6.2.0",
|
||||
"glob": "^9.3.5",
|
||||
"highlight.js": "^11.11.1",
|
||||
"mammoth": "^1.9.1",
|
||||
|
||||
@@ -156,6 +156,7 @@ userDefaults:
|
||||
disableQuickToggles: false # disable the quick toggles in the sidebar
|
||||
disableSearchOptions: false # disable the search options in the search bar
|
||||
stickySidebar: true # keep sidebar open when navigating
|
||||
hideFilesInTree: false # hide files in the sidebar tree navigation, when true, will show only directories.
|
||||
darkMode: true # should dark mode be enabled
|
||||
locale: "en" # language to use: eg. de, en, or fr
|
||||
viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact
|
||||
@@ -173,12 +174,14 @@ userDefaults:
|
||||
disableHideSidebar: false # keep sidebar open when previewing files
|
||||
image: true # show thumbnails for image files
|
||||
video: true # show thumbnails for video files
|
||||
audio: true # show thumbnails for audio files
|
||||
motionVideoPreview: true # show multiple frames for videos in thumbnail preview when hovering
|
||||
office: true # show thumbnails for office files
|
||||
popup: true # show larger popup preview when hovering over thumbnail
|
||||
autoplayMedia: true # autoplay media files in preview
|
||||
defaultMediaPlayer: false # disable the styled feature-rich media player for browser default
|
||||
folder: true # show thumbnails for folders that have previewable contents
|
||||
models: true # show live thumbnails for 3D models files
|
||||
permissions:
|
||||
api: false # allow api access
|
||||
admin: false # allow admin access
|
||||
@@ -191,6 +194,7 @@ userDefaults:
|
||||
loginMethod: "password" # login method to use: eg. password, proxy, oidc
|
||||
disableUpdateNotifications: false # disable update notifications banner for admin users
|
||||
deleteWithoutConfirming: false # delete files without confirmation
|
||||
deleteAfterArchive: true # delete source files after successful creation/extraction of archives
|
||||
fileLoading: # upload and download settings
|
||||
maxConcurrentUpload: 10
|
||||
uploadChunkSizeMb: 10
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<title>{{ .htmlVars.title }}</title>
|
||||
<title>"{{ .htmlVars.title }}"</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="{{ .htmlVars.favicon }}">
|
||||
|
||||
|
||||
@@ -132,7 +132,11 @@ export async function getApiKeys() {
|
||||
const apiPath = getApiPath('auth/token/list')
|
||||
return await fetchJSON(apiPath)
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Failed to get API tokens')
|
||||
// ignore 404 errors
|
||||
if (err.status !== 404) {
|
||||
notify.showError(err.message || 'Failed to get API tokens')
|
||||
throw err
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:class="{ 'dark-mode': isDarkMode, 'centered': centered }"
|
||||
:key="showCreate ? 'create-mode' : 'normal-mode'"
|
||||
>
|
||||
<div class="context-menu-header">
|
||||
<div v-if="!showLimitedOptions" class="context-menu-header">
|
||||
<div
|
||||
class="action button clickable"
|
||||
v-if="showCreateButton"
|
||||
@@ -208,6 +208,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showLimitedOptions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: null, // Array of item objects { name, path, source, isDir, type, ... }
|
||||
@@ -235,10 +239,7 @@ export default {
|
||||
return this.hasDownload && !this.req.isDir && !this.isShare;
|
||||
},
|
||||
showGoToItem() {
|
||||
return this.isDuplicateFinder && this.selectedCount == 1;
|
||||
},
|
||||
isDuplicateFinder() {
|
||||
return getters.currentView() === "duplicateFinder";
|
||||
return this.showLimitedOptions && this.selectedCount == 1;
|
||||
},
|
||||
permissions() {
|
||||
return getters.permissions();
|
||||
@@ -250,65 +251,65 @@ export default {
|
||||
return getters.isShare();
|
||||
},
|
||||
showCreateActions() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
return this.showCreate && !this.isSearchActive;
|
||||
},
|
||||
showInfo() {
|
||||
if (this.isDuplicateFinder) return this.selectedCount == 1;
|
||||
if (this.showLimitedOptions) return this.selectedCount == 1;
|
||||
return !this.showCreate && this.selectedCount == 1;
|
||||
},
|
||||
showDownload() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
return !this.showCreate && this.permissions.download && this.selectedCount > 0;
|
||||
},
|
||||
showArchive() {
|
||||
if (this.isDuplicateFinder || getters.isShare()) return false;
|
||||
if (this.showLimitedOptions || getters.isShare()) return false;
|
||||
if (!this.permissions.create) return false;
|
||||
return !this.showCreate && this.selectedCount > 0 && !this.showUnarchive;
|
||||
},
|
||||
showUnarchive() {
|
||||
if (this.isDuplicateFinder || getters.isShare()) return false;
|
||||
if (this.showLimitedOptions || getters.isShare()) return false;
|
||||
if (!this.permissions.create) return false;
|
||||
if (this.selectedCount !== 1) return false;
|
||||
const item = this.firstSelected;
|
||||
return item && isArchivePath(item.path || item.from || item.name);
|
||||
},
|
||||
showShareAction() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
return (this.showCreate || this.selectedCount == 1) && this.showShare;
|
||||
},
|
||||
showRename() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
return !this.showCreate && this.selectedCount == 1 && this.permissions.modify && !this.isSearchActive;
|
||||
},
|
||||
showCopy() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
return !this.showCreate && this.selectedCount > 0 && this.permissions.modify;
|
||||
},
|
||||
showOpenParentFolder() {
|
||||
return !this.showCreate && this.selectedCount == 1 && (this.isSearchActive || this.isDuplicateFinder);
|
||||
return !this.showCreate && this.selectedCount == 1 && (this.isSearchActive || this.showLimitedOptions);
|
||||
},
|
||||
showMove() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
return !this.showCreate && this.selectedCount > 0 && this.permissions.modify;
|
||||
},
|
||||
showSelectAll() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
return !this.showCreate && !this.isSearchActive && this.req?.items?.length > 0;
|
||||
},
|
||||
showCreateButton() {
|
||||
if (this.isDuplicateFinder || this.createOnly) return false;
|
||||
if (this.showLimitedOptions || this.createOnly) return false;
|
||||
return !this.isSearchActive && this.permissions.create && !this.isShare;
|
||||
},
|
||||
showDivider() {
|
||||
if (this.isDuplicateFinder || this.createOnly) return false;
|
||||
if (this.showLimitedOptions || this.createOnly) return false;
|
||||
if (getters.isShare()) {
|
||||
return state.shareInfo?.allowCreate
|
||||
}
|
||||
return state.user?.permissions?.create || state.user?.permissions?.share || state.user?.permissions?.admin;
|
||||
},
|
||||
showSelectMultiple() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
if (this.isMultiple || this.isSearchActive) {
|
||||
return false;
|
||||
}
|
||||
@@ -340,7 +341,7 @@ export default {
|
||||
return cv == "markdownViewer" && this.permissions.modify;
|
||||
},
|
||||
showDelete() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
if (this.selectedCount == 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -352,15 +353,7 @@ export default {
|
||||
return this.selectedCount > 0 && this.permissions.download;
|
||||
},
|
||||
isPreview() {
|
||||
const cv = getters.currentView();
|
||||
return (
|
||||
cv == "preview" ||
|
||||
cv == "onlyOfficeEditor" ||
|
||||
cv == "markdownViewer" ||
|
||||
cv == "epubViewer" ||
|
||||
cv == "docViewer" ||
|
||||
cv == "editor"
|
||||
);
|
||||
return getters.isPreviewView();
|
||||
},
|
||||
showSave() {
|
||||
const allowEdit = this.permissions.modify || (getters.isShare() && state.shareInfo.allowEdit);
|
||||
@@ -370,7 +363,7 @@ export default {
|
||||
return getters.currentPromptName() == "OverflowMenu";
|
||||
},
|
||||
showAccess() {
|
||||
if (this.isDuplicateFinder) return false;
|
||||
if (this.showLimitedOptions) return false;
|
||||
if (getters.isShare()) {
|
||||
return false;
|
||||
}
|
||||
@@ -439,8 +432,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
showInfoHover() {
|
||||
mutations.closeContextMenus();
|
||||
mutations.showHover({
|
||||
mutations.closeTopHover(); // close top hover to close overflow menu and replace any other open prompt
|
||||
mutations.showHover({ // from preview we can open the same prompt multiple times, which is undesirable.
|
||||
name: "info",
|
||||
props: {
|
||||
item: this.firstSelected,
|
||||
@@ -450,6 +443,7 @@ export default {
|
||||
goToItem() {
|
||||
const item = this.firstSelected;
|
||||
url.goToItem(item.source, item.path, {}, true);
|
||||
mutations.closeHovers();
|
||||
},
|
||||
hideTooltip() {
|
||||
mutations.hideTooltip();
|
||||
@@ -607,7 +601,7 @@ export default {
|
||||
}
|
||||
},
|
||||
toggleMultipleSelection() {
|
||||
mutations.setMultiple(!state.multiple);
|
||||
mutations.setMultiple(true);
|
||||
mutations.closeHovers();
|
||||
},
|
||||
startDownload() {
|
||||
@@ -616,7 +610,7 @@ export default {
|
||||
downloadFiles(items);
|
||||
},
|
||||
showDeleteHover() {
|
||||
mutations.closeContextMenus();
|
||||
mutations.closeTopHover();
|
||||
mutations.showHover({
|
||||
name: 'delete',
|
||||
props: {
|
||||
@@ -770,19 +764,11 @@ export default {
|
||||
openParentFolder() {
|
||||
const item = this.firstSelected;
|
||||
const parentPath = url.removeLastDir(item.path) || "/";
|
||||
url.goToItem(item.source, parentPath, {}, this.isDuplicateFinder);
|
||||
url.goToItem(item.source, parentPath, {}, this.showLimitedOptions);
|
||||
mutations.closeHovers();
|
||||
},
|
||||
selectAllItems() {
|
||||
if (state.req && state.req.items && state.req.items.length > 0) {
|
||||
// Clear current selection first
|
||||
mutations.resetSelected();
|
||||
// Add all items from current directory to selection by their indices
|
||||
state.req.items.forEach((item, index) => {
|
||||
mutations.addSelected(index);
|
||||
});
|
||||
// Close the context menu
|
||||
mutations.closeContextMenus();
|
||||
}
|
||||
mutations.selectAllItems()
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -141,7 +141,7 @@ export default {
|
||||
],
|
||||
typeSelect: [
|
||||
{ label: this.$t("general.photos"), value: "type:image" },
|
||||
{ label: this.$t("general.audio"), value: "type:audio" },
|
||||
{ label: this.$t("general.audios"), value: "type:audio" },
|
||||
{ label: this.$t("general.videos"), value: "type:video" },
|
||||
{ label: this.$t("general.documents"), value: "type:doc" },
|
||||
{ label: this.$t("general.archives"), value: "type:archive" },
|
||||
@@ -293,7 +293,7 @@ export default {
|
||||
let result = url.extractSourceFromPath(decodeURIComponent(state.route.path));
|
||||
if (this.selectedSource === "" || result.source === this.selectedSource) {
|
||||
return result.path;
|
||||
} else {
|
||||
} else {
|
||||
return "/"; // if searching on non-current source, search the whole thing
|
||||
}
|
||||
},
|
||||
@@ -307,6 +307,7 @@ export default {
|
||||
props: {
|
||||
posX: event.clientX,
|
||||
posY: event.clientY,
|
||||
showLimitedOptions: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -291,10 +291,10 @@ export default {
|
||||
action: buttonAction
|
||||
}]
|
||||
});
|
||||
mutations.closeHovers();
|
||||
mutations.closeTopHover();
|
||||
mutations.setReload(true);
|
||||
} catch (error) {
|
||||
mutations.closeHovers();
|
||||
mutations.closeTopHover();
|
||||
notify.showErrorToast(this.$t("prompts.moveFailed"));
|
||||
console.log("Move failed", e);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
:isSelectedProp="selected === item.path"
|
||||
:clickable="false"
|
||||
:forceFilesApi="!!browseSource"
|
||||
:showLimitedOptions="true"
|
||||
@click.prevent="(event) => handleItemClick(item, index, event)"
|
||||
@dblclick.prevent="(event) => handleItemDblClick(item, index, event)"
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
'current-item': isCurrentItem(node),
|
||||
'has-children': node.childrenCount > 0,
|
||||
'drag-over': node.dragOver || isSelected(node),
|
||||
'hidden-file': node.isHidden,
|
||||
}"
|
||||
@click="handleNodeClick(node)"
|
||||
@contextmenu.prevent="handleContextMenu($event, node)"
|
||||
@@ -38,7 +39,7 @@
|
||||
<Icon
|
||||
:mimetype="node.isDir ? 'directory' : node.type"
|
||||
:filename="node.name"
|
||||
:isShared="isShare"
|
||||
:isShared="node.isShared"
|
||||
class="node-icon"
|
||||
/>
|
||||
<span class="node-name" :title="node.name">
|
||||
@@ -54,7 +55,8 @@
|
||||
:currentPath="currentPath"
|
||||
/>
|
||||
<div v-else-if="node.expanded && node.children && node.children.length === 0" class="tree-empty-folder">
|
||||
{{ $t('files.lonely') }}
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
</div>
|
||||
<div v-else-if="node.expanded && node.loading" class="tree-loading">
|
||||
<LoadingSpinner size="small" mode="placeholder" />
|
||||
@@ -133,6 +135,10 @@ export default {
|
||||
canModify() {
|
||||
return getters.permissions()?.modify;
|
||||
},
|
||||
showFiles() {
|
||||
// Defaults to true
|
||||
return !state.user?.hideFilesInTree;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentSource: {
|
||||
@@ -160,6 +166,14 @@ export default {
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
showFiles: {
|
||||
handler() {
|
||||
if (this.isRootInstance) {
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
immediate: false,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.unwatchReload = this.$watch(
|
||||
@@ -203,13 +217,19 @@ export default {
|
||||
}
|
||||
},
|
||||
async fetchItems(path) {
|
||||
let items = [];
|
||||
if (this.isShare) {
|
||||
const res = await resourcesApi.fetchFilesPublic(path, this.shareHash, state.shareInfo.password, false, false, true);
|
||||
return res.items || [];
|
||||
const res = await resourcesApi.fetchFilesPublic(path, this.shareHash, state.shareInfo?.password, false, false, true);
|
||||
items = res.items || [];
|
||||
} else {
|
||||
const res = await resourcesApi.fetchFiles(this.currentSource, path, false, false, true);
|
||||
return res.items || [];
|
||||
items = res.items || [];
|
||||
}
|
||||
// Filter out files if showFiles is false
|
||||
if (!this.showFiles) {
|
||||
items = items.filter(item => item.type === 'directory');
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
createNode(item) {
|
||||
@@ -224,7 +244,9 @@ export default {
|
||||
children: null,
|
||||
childrenCount: 0,
|
||||
childrenError: null,
|
||||
isShared: item.isShared,
|
||||
dragOver: false, // For drag highlight
|
||||
isHidden: item.hidden,
|
||||
...item
|
||||
};
|
||||
},
|
||||
@@ -548,10 +570,10 @@ export default {
|
||||
}]
|
||||
};
|
||||
notify.showSuccess(this.$t('prompts.moveSuccess'), buttonProps);
|
||||
mutations.closeHovers();
|
||||
mutations.closeTopHover();
|
||||
mutations.setReload(true);
|
||||
} catch (error) {
|
||||
mutations.closeHovers();
|
||||
mutations.closeTopHover();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -563,7 +585,7 @@ export default {
|
||||
const overwrite = option === 'overwrite';
|
||||
const rename = option === 'rename';
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
mutations.closeTopHover();
|
||||
await performAction(overwrite, rename);
|
||||
},
|
||||
});
|
||||
@@ -658,6 +680,10 @@ export default {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tree-node.hidden-file {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tree-node:hover {
|
||||
background-color: var(--surfaceSecondary);
|
||||
}
|
||||
@@ -750,11 +776,22 @@ export default {
|
||||
}
|
||||
|
||||
.tree-empty-folder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
padding: 0.5em 1em;
|
||||
margin-left: 1.2em;
|
||||
text-align: center;
|
||||
color: var(--textSecondary);
|
||||
font-style: italic;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.tree-empty-folder i {
|
||||
font-size: 1.4em;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
<i v-if="isShared" class="material-icons overlay-icon">group</i>
|
||||
<!-- Preview content: image, 3D, or fallback -->
|
||||
<img v-if="hasPreviewImage" :key="imageTargetSrc" :src="imageDisplaySrc" ref="thumbnail" />
|
||||
<ThreeJs v-else-if="shouldUse3DPreview && !threeJsError"
|
||||
<ThreeJs v-else-if="shouldUse3DPreview && !threeJsError"
|
||||
:key="`3d-${path}-${gallerySizeKey}`"
|
||||
:fbdata="{
|
||||
name: filename,
|
||||
@@ -20,7 +20,7 @@
|
||||
:add-load-delay="true"
|
||||
@error="handle3DError" />
|
||||
</span>
|
||||
|
||||
|
||||
<!-- Regular material icon (no preview) -->
|
||||
<span v-else class="image-preview">
|
||||
<i :class="[classes, { active: active, clickable: clickable }]"> {{ materialIcon }} </i>
|
||||
@@ -122,12 +122,18 @@ export default {
|
||||
return false;
|
||||
}
|
||||
const simpleType = this.getIconForType().simpleType;
|
||||
if (simpleType === "audio" && !getters.previewPerms().audio) {
|
||||
return false;
|
||||
}
|
||||
if (simpleType === "video" && !getters.previewPerms().video) {
|
||||
return false;
|
||||
}
|
||||
if (simpleType === "image" && !getters.previewPerms().image) {
|
||||
return false;
|
||||
}
|
||||
if (simpleType === "ebook" && !getters.previewPerms().image) {
|
||||
return false;
|
||||
}
|
||||
// office files
|
||||
if ((simpleType === "document" || simpleType === "text") && !getters.previewPerms().office) {
|
||||
return false;
|
||||
@@ -136,7 +142,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
// 3D models - show preview thumbnails (if backend provides them)
|
||||
if (simpleType === "3d-model" && !getters.previewPerms().image) {
|
||||
if (simpleType === "3d-model" && !getters.previewPerms().models) {
|
||||
return false;
|
||||
}
|
||||
return this.imageState !== 'error' && !this.disablePreviewExt && !this.officeFileDisabled;
|
||||
@@ -213,10 +219,11 @@ export default {
|
||||
shouldUse3DPreview() {
|
||||
// Check if we should use 3D preview instead of regular icon
|
||||
if (!this.isFile || !this.size || !this.path) return false;
|
||||
|
||||
if (!getters.previewPerms().models) return false;
|
||||
|
||||
const MAX_SIZE = 250 * 1024; // 250KB in bytes
|
||||
if (this.size > MAX_SIZE) return false;
|
||||
|
||||
|
||||
const typeInfo = this.getIconForType();
|
||||
return typeInfo.simpleType === '3d-model';
|
||||
},
|
||||
@@ -365,13 +372,12 @@ export default {
|
||||
},
|
||||
updateImageTargetSrc() {
|
||||
let newSrc = this.thumbnailUrl || PLACEHOLDER_URL;
|
||||
|
||||
// If we need large thumbnails and have a thumbnail URL, append &size=large
|
||||
// Otherwise use the URL as-is (defaults to small)
|
||||
if (this.thumbnailUrl && this.showLargeIcon) {
|
||||
newSrc = this.thumbnailUrl + "&size=large";
|
||||
}
|
||||
|
||||
|
||||
if (this.imageTargetSrc !== newSrc) {
|
||||
this.imageTargetSrc = newSrc;
|
||||
}
|
||||
@@ -525,6 +531,10 @@ export default {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.brown-icons {
|
||||
color: brown;
|
||||
}
|
||||
|
||||
#listingView.gallery .listing-item i.white-icons,
|
||||
.active.white-icons {
|
||||
color: var(--activeWhiteIcon);
|
||||
@@ -558,11 +568,6 @@ export default {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.simple-icons {
|
||||
color: white;
|
||||
font-size: 1.5em !important;
|
||||
}
|
||||
|
||||
/* Unified .image-preview container - works universally, always square */
|
||||
.image-preview {
|
||||
width: var(--icon-size);
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<span>{{ $t("files.duration") }}</span>
|
||||
<i v-if="durationSorted" class="material-icons">{{ durationIcon }}</i>
|
||||
</p>
|
||||
<span v-if="quickDownloadEnabled" class="placeholder"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -112,6 +113,18 @@ export default {
|
||||
}
|
||||
return "arrow_upward";
|
||||
},
|
||||
quickDownloadEnabled() {
|
||||
// @ts-ignore
|
||||
if (state.isMobile) {
|
||||
return false
|
||||
}
|
||||
if (getters.isShare()) {
|
||||
// @ts-ignore
|
||||
return state.shareInfo?.quickDownload;
|
||||
}
|
||||
// @ts-ignore
|
||||
return state.user?.quickDownload && !this.galleryView;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sort(field) {
|
||||
@@ -154,18 +167,13 @@ export default {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-right: 1.5em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -176,28 +184,24 @@ span {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.size {
|
||||
box-sizing: border-box;
|
||||
.name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.desktop-view .size,
|
||||
.desktop-view .modified,
|
||||
.desktop-view .duration {
|
||||
min-width: 10%;
|
||||
flex: 0 !important;
|
||||
}
|
||||
|
||||
.size,
|
||||
.modified,
|
||||
.duration {
|
||||
margin-left: auto;
|
||||
padding-right: 1em;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.desktop-view {
|
||||
justify-content: unset !important;
|
||||
}
|
||||
|
||||
/* Desktop-specific column widths */
|
||||
.desktop-view .name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.desktop-view .size {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 1.5em;
|
||||
|
||||
@@ -47,10 +47,9 @@
|
||||
</div>
|
||||
|
||||
<div class="text">
|
||||
<p :class="{ adjustment: quickDownloadEnabled }" class="name">{{ displayName }}</p>
|
||||
<p class="name">{{ displayName }}</p>
|
||||
<p
|
||||
class="size"
|
||||
:class="{ adjustment: quickDownloadEnabled }"
|
||||
:data-order="humanSize"
|
||||
>
|
||||
{{ humanSize }}
|
||||
@@ -66,7 +65,6 @@
|
||||
:filename="name"
|
||||
:hasPreview="hasPreview"
|
||||
mimetype="file_download"
|
||||
style="padding-right: 0.5em"
|
||||
class="download-icon"
|
||||
role="button"
|
||||
aria-label="Download"
|
||||
@@ -74,6 +72,10 @@
|
||||
:clickable=true
|
||||
:isShared="isShared"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="quickDownloadPlaceholder"
|
||||
class="placeholder"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
v-else
|
||||
@@ -116,10 +118,9 @@
|
||||
</div>
|
||||
|
||||
<div class="text">
|
||||
<p :class="{ adjustment: quickDownloadEnabled }" class="name">{{ displayName }}</p>
|
||||
<p class="name">{{ displayName }}</p>
|
||||
<p
|
||||
class="size"
|
||||
:class="{ adjustment: quickDownloadEnabled }"
|
||||
:data-order="humanSize"
|
||||
>
|
||||
{{ humanSize }}
|
||||
@@ -200,6 +201,14 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showLimitedOptions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disableContextMenu: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
displayName() {
|
||||
@@ -213,11 +222,20 @@ export default {
|
||||
// @ts-ignore
|
||||
if (getters.isShare()) {
|
||||
// @ts-ignore
|
||||
return state.shareInfo?.quickDownload && !this.isDir;
|
||||
return state.shareInfo?.quickDownload && !this.galleryView && !this.isDir;
|
||||
}
|
||||
// @ts-ignore
|
||||
return state.user?.quickDownload && !this.galleryView && !this.isDir;
|
||||
},
|
||||
quickDownloadPlaceholder() {
|
||||
// @ts-ignore
|
||||
if (getters.isShare()) {
|
||||
// @ts-ignore
|
||||
return state.shareInfo?.quickDownload && !this.galleryView && this.isDir;
|
||||
}
|
||||
// @ts-ignore
|
||||
return state.user?.quickDownload && !this.galleryView && getters.viewMode() !== "icons" && getters.viewMode() !== "normal" && this.isDir;
|
||||
},
|
||||
isHiddenNotSelected() {
|
||||
return !this.isSelected && this.reducedOpacity;
|
||||
},
|
||||
@@ -415,20 +433,15 @@ export default {
|
||||
},
|
||||
/** @param {MouseEvent} event */
|
||||
onRightClick(event) {
|
||||
if (!this.updateGlobalState) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.preventDefault(); // Prevent default context menu
|
||||
// If one or fewer items are selected, reset the selection
|
||||
if (this.updateGlobalState) {
|
||||
// If one or fewer items are selected, reset the selection
|
||||
if (!state.multiple && getters.selectedCount() < 2) {
|
||||
mutations.resetSelected();
|
||||
// @ts-ignore
|
||||
mutations.addSelected(this.index);
|
||||
}
|
||||
} else {
|
||||
|
||||
// Build full item object similar to Search.vue
|
||||
const selectedItem = {
|
||||
name: this.name,
|
||||
@@ -440,16 +453,21 @@ export default {
|
||||
path: this.path,
|
||||
url: this.path,
|
||||
index: this.index,
|
||||
};
|
||||
};
|
||||
mutations.resetSelected();
|
||||
// @ts-ignore
|
||||
mutations.addSelected(selectedItem);
|
||||
}
|
||||
|
||||
if (this.disableContextMenu) {
|
||||
return;
|
||||
}
|
||||
mutations.showHover({
|
||||
name: "ContextMenu",
|
||||
props: {
|
||||
posX: event.clientX,
|
||||
posY: event.clientY,
|
||||
showLimitedOptions: this.showLimitedOptions,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -616,11 +634,11 @@ export default {
|
||||
};
|
||||
notify.showSuccess(this.$t("prompts.moveSuccess"), buttonProps);
|
||||
// Close the prompt after successful operation and reload items for reflect the changes
|
||||
mutations.closeHovers();
|
||||
mutations.closeTopHover();
|
||||
mutations.setReload(true);
|
||||
} catch (error) {
|
||||
// Close the prompt and let error handling continue
|
||||
mutations.closeHovers();
|
||||
mutations.closeTopHover();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -638,7 +656,7 @@ export default {
|
||||
const rename = option === "rename";
|
||||
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
mutations.closeTopHover();
|
||||
await action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
@@ -769,9 +787,9 @@ export default {
|
||||
// Local selection handling - emit events instead of updating global state
|
||||
if (event.shiftKey) {
|
||||
// Shift-click: select range (emit event for parent to handle)
|
||||
this.$emit('selectRange', {
|
||||
this.$emit('selectRange', {
|
||||
startIndex: state.lastSelectedIndex !== null ? state.lastSelectedIndex : this.index,
|
||||
endIndex: this.index
|
||||
endIndex: this.index
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -786,7 +804,7 @@ export default {
|
||||
if (!this.updateGlobalState) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if state.req.items exists and has the item at this index
|
||||
// This prevents errors when ListingItem is used outside of the main file listing (e.g., duplicate finder)
|
||||
let previousHistoryItem = null;
|
||||
@@ -805,7 +823,6 @@ export default {
|
||||
|
||||
<style>
|
||||
.download-icon {
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
color: var(--secondaryColor);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
sourceInfo() {
|
||||
if (state.multiple) {
|
||||
return false;
|
||||
}
|
||||
return state.popupPreviewSourceInfo;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -166,4 +166,9 @@ export default {
|
||||
.button i {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -46,11 +46,9 @@
|
||||
max="9"
|
||||
/>
|
||||
<div class="archive-options settings-items">
|
||||
<ToggleSwitch
|
||||
class="item"
|
||||
v-model="deleteAfter"
|
||||
:name="$t('prompts.archiveDeleteAfter')"
|
||||
/>
|
||||
<ToggleSwitch class="item" v-model="deleteAfter"
|
||||
:name="$t('profileSettings.deleteAfterArchive')"
|
||||
:description="$t('profileSettings.deleteAfterArchiveDescription')" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -97,7 +95,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mutations } from "@/store";
|
||||
import { state, mutations } from "@/store";
|
||||
import { notify } from "@/notify";
|
||||
import { resourcesApi } from "@/api";
|
||||
import { goToItem } from "@/utils/url";
|
||||
@@ -131,9 +129,14 @@ export default {
|
||||
compression: 0,
|
||||
creating: false,
|
||||
showFileList: false,
|
||||
deleteAfter: true,
|
||||
deleteAfter: state.user?.deleteAfterArchive == true,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
deleteAfter(newVal) {
|
||||
mutations.updateCurrentUser({ deleteAfterArchive: newVal });
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.currentPath) {
|
||||
this.destPath = this.currentPath.replace(/\/+$/, "") || "/";
|
||||
|
||||
@@ -327,7 +327,8 @@ export default {
|
||||
|
||||
// Only close prompts and reload on success (or partial success)
|
||||
mutations.setReload(true);
|
||||
mutations.closeTopHover();
|
||||
mutations.closeTopHover(); // close conflict prompt
|
||||
mutations.closeTopHover(); // close moveCopy prompt after conflict is resolved
|
||||
mutations.setSearch(false);
|
||||
|
||||
// Only show success notification if there were no failures (or partial success was already shown)
|
||||
|
||||
@@ -31,8 +31,13 @@
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isPickingFile">
|
||||
<div aria-label="share-path" class="searchContext button"> {{ $t('general.path', { suffix: ':' }) }} {{
|
||||
<div v-if="isFiles" aria-label="share-path" class="searchContext button"> {{ $t('general.path', { suffix: ':' }) }} {{
|
||||
displayPath }}</div>
|
||||
<div v-else>
|
||||
<a :href="link.sourceURL" class="button button--block">
|
||||
{{ $t('buttons.goToSource') }}
|
||||
</a>
|
||||
</div>
|
||||
<p> {{ $t('share.notice') }} </p>
|
||||
|
||||
<div v-if="listing">
|
||||
@@ -462,6 +467,9 @@ export default {
|
||||
isListing() {
|
||||
return getters.isListing(); // Access getter directly from the store
|
||||
},
|
||||
isFiles() {
|
||||
return getters.isFiles(); // Access getter directly from the store
|
||||
},
|
||||
url() {
|
||||
if (state.isSearchActive) {
|
||||
const file = /** @type {FilebrowserFile} */ (this.selected[0]);
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
class="link-drag-handle">
|
||||
<i class="material-icons">drag_indicator</i>
|
||||
</div>
|
||||
<div class="link-icon">
|
||||
<div v-if="link.category === 'divider'" class="link-icon">
|
||||
<i class="material-icons">horizontal_rule</i>
|
||||
</div>
|
||||
<div v-else class="link-icon">
|
||||
<i :class="getIconClass(link.icon)">{{ link.icon }}</i>
|
||||
</div>
|
||||
<div class="link-details">
|
||||
@@ -64,6 +67,7 @@
|
||||
<option value="share">{{ $t('general.share') }}</option>
|
||||
<option v-if="context === 'user'" value="tool">{{ $t('general.tool') }}</option>
|
||||
<option value="custom">{{ $t('sidebar.customLink') }}</option>
|
||||
<option value="divider">{{ $t('general.divider') }}</option>
|
||||
<option v-if="context === 'share'" value="shareInfo">{{ $t('share.shareInfo') }}</option>
|
||||
<option v-if="context === 'share'" value="download">{{ $t('general.download') }}</option>
|
||||
</select>
|
||||
@@ -171,6 +175,13 @@
|
||||
<p>{{ $t('share.downloadDescription') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Divider - special visual separator -->
|
||||
<div v-if="newLink.category === 'divider'" class="form-group">
|
||||
<p>{{ $t('sidebar.linkName') }}</p>
|
||||
<input aria-label="Link Name" v-model="newLink.name" type="text" class="input"
|
||||
:placeholder="$t('sidebar.dividerNamePlaceholder')" />
|
||||
</div>
|
||||
|
||||
<!-- Custom Link Input -->
|
||||
<div v-if="newLink.category === 'custom'" class="form-group">
|
||||
<p>{{ $t('sidebar.linkName') }}</p>
|
||||
@@ -182,8 +193,8 @@
|
||||
:placeholder="$t('sidebar.linkUrlPlaceholder')" />
|
||||
</div>
|
||||
|
||||
<!-- Icon Selection - Available for ALL link types -->
|
||||
<div v-if="newLink.category" class="form-group">
|
||||
<!-- Icon Selection - Available for ALL link types except divider -->
|
||||
<div v-if="newLink.category && newLink.category !== 'divider'" class="form-group">
|
||||
<p>{{ $t('sidebar.linkIcon') }}</p>
|
||||
<div class="icon-input-group">
|
||||
<input v-model="newLink.icon" type="text" class="input icon-input"
|
||||
@@ -298,8 +309,10 @@ export default {
|
||||
isNewLinkValid() {
|
||||
if (!this.newLink.category) return false;
|
||||
|
||||
// Special link types for shares don't need additional validation
|
||||
if (this.newLink.category === "shareInfo" || this.newLink.category === "download") {
|
||||
// Special link types that don't need additional validation
|
||||
if (this.newLink.category === "shareInfo" ||
|
||||
this.newLink.category === "download" ||
|
||||
this.newLink.category === "divider") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -457,10 +470,16 @@ export default {
|
||||
share: this.$t('general.share'),
|
||||
shareInfo: this.$t('share.shareInfo'),
|
||||
download: this.$t('general.download'),
|
||||
divider: this.$t('general.divider'),
|
||||
};
|
||||
return labels[category] || category;
|
||||
},
|
||||
getLinkDisplayName(link) {
|
||||
// For dividers, show the name or a default
|
||||
if (link.category === 'divider') {
|
||||
return link.name || this.$t('general.divider');
|
||||
}
|
||||
|
||||
// Check if the name looks like a translation key that needs translating
|
||||
if (link.category === 'shareInfo' && link.name === 'share.shareInfo') {
|
||||
return this.$t('share.shareInfo');
|
||||
@@ -612,6 +631,11 @@ export default {
|
||||
} else if (this.newLink.category === "download") {
|
||||
// Download is a special action link
|
||||
linkData.target = "#";
|
||||
} else if (this.newLink.category === "divider") {
|
||||
// Divider is a visual separator with no action
|
||||
linkData.target = "#";
|
||||
linkData.name = this.newLink.name || ""; // Keep the custom name if provided
|
||||
linkData.icon = ""; // Dividers don't need an icon
|
||||
} else if (this.newLink.category === "custom") {
|
||||
linkData.target = this.processCustomUrl(this.newLink.target);
|
||||
} else if (this.isSourceCategory(this.newLink.category)) {
|
||||
|
||||
@@ -26,11 +26,9 @@
|
||||
{{ $t("general.path", { suffix: ":" }) }} {{ destPath }}{{ destSource ? ` (${destSource})` : "" }}
|
||||
</div>
|
||||
<div class="unarchive-options settings-items">
|
||||
<ToggleSwitch
|
||||
class="item"
|
||||
v-model="deleteAfter"
|
||||
:name="$t('prompts.unarchiveDeleteAfter')"
|
||||
/>
|
||||
<ToggleSwitch class="item" v-model="deleteAfter"
|
||||
:name="$t('profileSettings.deleteAfterArchive')"
|
||||
:description="$t('profileSettings.deleteAfterArchiveDescription')" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -77,7 +75,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mutations } from "@/store";
|
||||
import { state, mutations } from "@/store";
|
||||
import { url } from "@/utils";
|
||||
import { notify } from "@/notify";
|
||||
import { resourcesApi } from "@/api";
|
||||
@@ -100,11 +98,17 @@ export default {
|
||||
destPath: "/",
|
||||
destSource: null,
|
||||
destType: null,
|
||||
deleteAfter: true,
|
||||
deleteAfter: state.user?.deleteAfterArchive == true,
|
||||
isLoading: false,
|
||||
showFileList: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
deleteAfter(newVal) {
|
||||
// Update the user preference in real time
|
||||
mutations.updateCurrentUser({ deleteAfterArchive: newVal });
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.destPath = this.parentPath || "/";
|
||||
this.destSource = this.itemSource;
|
||||
|
||||
@@ -338,7 +338,8 @@ export default {
|
||||
await usersApi.remove(this.user.id);
|
||||
notify.showSuccessToast(this.$t("settings.userDeleted"));
|
||||
eventBus.emit('usersChanged');
|
||||
mutations.closeTopHover();
|
||||
mutations.closeTopHover(); // close delete user prompt
|
||||
mutations.closeTopHover(); // close user prompt since user doens't exist anymore
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notify.showErrorToast(this.$t("settings.userDeleteFailed"));
|
||||
|
||||
@@ -147,7 +147,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
openContextMenu() {
|
||||
console.log("openContextMenu")
|
||||
mutations.resetSelected();
|
||||
mutations.showHover({
|
||||
name: "ContextMenu",
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
<template>
|
||||
<div class="sidebar-links card">
|
||||
<!-- Header - sticks always at the top -->
|
||||
<div class="sidebar-links-header" :class="{ 'no-edit-options': isShare, 'with-top-spacing': isShare && !disableShareCard }">
|
||||
<i v-if="!isShare" @click="goHome()" class="material-icons action" :title="$t('general.home')">home</i>
|
||||
|
||||
<!-- Links section header -->
|
||||
<div class="sidebar-links-header"
|
||||
:class="{ 'with-top-spacing': isShare && !disableShareCard }">
|
||||
<i :class="{ 'disabled': !isLoggedIn }" @click="goHome()" class="material-icons action">home</i>
|
||||
<!-- Mode button (is the title) -->
|
||||
<button @click="cycleMode" class="mode-toggle" :title="$t('sidebar.switchMode')">
|
||||
<button @click="cycleMode" class="mode-toggle" :title="$t('sidebar.switchMode')" @mouseenter="showTooltip($event, $t('sidebar.switchMode'))" @mouseleave="hideTooltip">
|
||||
{{ mode === 'links' ? $t('general.links') : $t('general.navigation') }}
|
||||
</button>
|
||||
<!-- Edit button always visible - hidden in shares -->
|
||||
<i v-if="!isShare"
|
||||
@mouseenter="showTooltip($event, $t('sidebar.customizeLinks'))"
|
||||
@mouseleave="hideTooltip"
|
||||
@click="openSidebarLinksPrompt"
|
||||
class="material-icons action">edit</i>
|
||||
<i v-if="isShare" aria-label="Edit Share" @mouseenter="showTooltip($event, editShareText())" @mouseleave="hideTooltip"
|
||||
:class="{ 'disabled': !canEdit }"
|
||||
@click="showEditShareHover" class="material-icons action">edit</i>
|
||||
<i v-else @mouseenter="showTooltip($event, $t('sidebar.customizeLinks'))" @mouseleave="hideTooltip"
|
||||
@click="openSidebarLinksPrompt" class="material-icons action">edit</i>
|
||||
</div>
|
||||
<!-- Scrollable Content Area -->
|
||||
<div class="sidebar-links-content">
|
||||
@@ -24,8 +25,21 @@
|
||||
</div>
|
||||
<transition-group name="expand" tag="div" class="inner-card">
|
||||
<template v-for="(link, index) in sidebarLinksToDisplay" :key="`link-${index}-${link.category}`">
|
||||
<!-- Divider: renders as hr line or text label -->
|
||||
<div v-if="link.category === 'divider'" class="sidebar-divider-container">
|
||||
<hr v-if="!link.name || link.name.toLowerCase() === 'divider'" class="sidebar-divider" />
|
||||
<span v-else class="sidebar-divider-text">{{ link.name }}</span>
|
||||
</div>
|
||||
<!-- Source Location custom link -->
|
||||
<a v-else-if="link.category === 'custom' && link.name === 'sourceLocation'" :href="link.target"
|
||||
:aria-label="link.name" class="action button sidebar-link-button" @click.prevent="handleLinkClick(link)">
|
||||
<div class="link-container">
|
||||
<i class="material-icons link-icon">open_in_new</i>
|
||||
<span>{{ $t('buttons.goToSource') }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Source-type links (source, source-minimal, source-alt, source-hybrid, source-hybrid-2); usage bar hidden for source-minimal -->
|
||||
<a v-if="link.category === 'source' || link.category === 'source-minimal' || link.category === 'source-alt' || link.category === 'source-hybrid' || link.category === 'source-hybrid-2'" :href="getLinkHref(link)"
|
||||
<a v-else-if="link.category === 'source' || link.category === 'source-minimal' || link.category === 'source-alt' || link.category === 'source-hybrid' || link.category === 'source-hybrid-2'" :href="getLinkHref(link)"
|
||||
class="action button source-button sidebar-link-button" :class="{
|
||||
active: isLinkActive(link),
|
||||
disabled: !isLinkAccessible(link)
|
||||
@@ -81,20 +95,11 @@
|
||||
<a v-else :aria-label="link.name" :href="getLinkHref(link)" class="action button sidebar-link-button"
|
||||
:class="{ active: isLinkActive(link) }" @click.prevent="handleLinkClick(link)">
|
||||
<div class="link-container">
|
||||
<i :class="getIconClass(link.icon) + ' link-icon'">{{ link.icon }}</i>
|
||||
<i v-if="link.icon" :class="getIconClass(link.icon) + ' link-icon'">{{ link.icon }}</i>
|
||||
<span>{{ link.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
<!-- Edit Share link - only shown when viewing a share and user has share permissions -->
|
||||
<a v-if="isShare && canEditShare" :aria-label="$t('general.edit', { suffix: ' ' + $t('general.share') })" href="#"
|
||||
class="action button sidebar-link-button"
|
||||
@click.prevent="showEditShareHover">
|
||||
<div class="link-container">
|
||||
<i class="material-icons link-icon">edit</i>
|
||||
<span>{{ $t("general.edit", { suffix: " " + $t("general.share") }) }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</transition-group>
|
||||
</template>
|
||||
<!-- Navigation Mode -->
|
||||
@@ -126,7 +131,7 @@
|
||||
warning
|
||||
</i>
|
||||
<!-- Source name -->
|
||||
<span>{{ activeSource }}</span>
|
||||
<span>{{ activeSourceLink.name }}</span>
|
||||
<i v-if="hasUsageInfo(activeSourceLink)"
|
||||
class="no-select material-symbols-outlined tooltip-info-icon"
|
||||
@mouseenter="showSourceTooltip($event, activeSourceInfo)"
|
||||
@@ -161,9 +166,9 @@
|
||||
<!-- Source Dropdown -->
|
||||
<transition name="dropdown">
|
||||
<div v-if="showSourceDropdown" class="source-dropdown" ref="dropdown">
|
||||
<div v-for="sourceName in sourceNames" :key="sourceName" class="dropdown-item"
|
||||
@click="selectSource(sourceName)">
|
||||
{{ sourceName }}
|
||||
<div v-for="item in dropdownSourceItems" :key="item.rawName" class="dropdown-item"
|
||||
@click="selectSource(item.rawName)">
|
||||
{{ item.displayName }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -202,6 +207,12 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canEdit() {
|
||||
return state.shareInfo?.canEditShare || false;
|
||||
},
|
||||
isLoggedIn() {
|
||||
return state.user?.username !== 'anonymous';
|
||||
},
|
||||
currentSource() {
|
||||
return state.req?.source || null;
|
||||
},
|
||||
@@ -228,10 +239,6 @@ export default {
|
||||
// Check if user has customized their links
|
||||
return this.user?.sidebarLinks && this.user.sidebarLinks.length > 0;
|
||||
},
|
||||
canEditShare() {
|
||||
// Check if user is logged in and has share permissions
|
||||
return state.user && state.user.permissions && state.user.permissions.share;
|
||||
},
|
||||
// Share info card props
|
||||
disableShareCard() {
|
||||
return state.shareInfo?.disableShareCard;
|
||||
@@ -293,6 +300,15 @@ export default {
|
||||
sourceName: this.activeSource,
|
||||
};
|
||||
},
|
||||
dropdownSourceItems() {
|
||||
return this.sourceNames.map(rawName => {
|
||||
const customLink = this.sourceLinkMap[rawName];
|
||||
return {
|
||||
rawName,
|
||||
displayName: customLink ? customLink.name : rawName,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('click', this.closeDropdown);
|
||||
@@ -342,6 +358,9 @@ export default {
|
||||
return baseURL + target;
|
||||
},
|
||||
goHome() {
|
||||
if (!this.isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
this.$router.push('/');
|
||||
},
|
||||
getDefaultLinks() {
|
||||
@@ -436,10 +455,15 @@ export default {
|
||||
return;
|
||||
}
|
||||
// For all other links (tools, custom, share), navigate using target directly
|
||||
if (link.target) {
|
||||
const currentBase = this.$route.path;
|
||||
const targetBase = link.target.split('?')[0]; // strip any query from target -- this to avoid an error when navigating back with browser history
|
||||
|
||||
if (currentBase === targetBase) {
|
||||
this.$router.replace(link.target);
|
||||
} else {
|
||||
this.$router.push(link.target);
|
||||
mutations.closeTopHover();
|
||||
}
|
||||
mutations.closeTopHover();
|
||||
},
|
||||
goToDownload() {
|
||||
// Check if we're in a directory with multiple items
|
||||
@@ -480,6 +504,9 @@ export default {
|
||||
mutations.closeTopHover();
|
||||
},
|
||||
openSidebarLinksPrompt() {
|
||||
if (!this.isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
mutations.showHover({
|
||||
name: "SidebarLinks",
|
||||
});
|
||||
@@ -536,6 +563,9 @@ export default {
|
||||
return buildIndexInfoTooltipHTML(info, this.$t, state.user.locale);
|
||||
},
|
||||
async showEditShareHover() {
|
||||
if (!this.canEdit) {
|
||||
return;
|
||||
}
|
||||
// Get the current share hash and fetch full share details
|
||||
const shareHash = state.shareInfo?.hash;
|
||||
if (!shareHash) {
|
||||
@@ -648,15 +678,12 @@ export default {
|
||||
}
|
||||
|
||||
.share-info-section {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0.5em;
|
||||
padding-bottom: 0.25em;
|
||||
border-bottom: 1px solid var(--borderColor);
|
||||
}
|
||||
|
||||
.with-top-spacing {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.usage-info .vue-simple-progress {
|
||||
border-style: solid;
|
||||
border-color: var(--surfaceSecondary);
|
||||
@@ -675,7 +702,8 @@ export default {
|
||||
.sidebar-link-button {
|
||||
margin: 0;
|
||||
margin-top: 0.25em;
|
||||
padding: 0;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
border-radius: 0.5em;
|
||||
justify-content: flex-start;
|
||||
max-width: 98%;
|
||||
@@ -893,4 +921,25 @@ a.sidebar-link-button {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.sidebar-divider-container {
|
||||
width: 100%;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.sidebar-divider {
|
||||
width: 50%;
|
||||
border: none;
|
||||
border-top: 0.1em solid var(--divider);
|
||||
}
|
||||
|
||||
.sidebar-divider-text {
|
||||
display: flex;
|
||||
font-size: .85em;
|
||||
color: var(--textSecondary);
|
||||
letter-spacing: .05em;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -58,7 +58,7 @@ body.rtl .listing-items {
|
||||
}
|
||||
|
||||
.listing-item p {
|
||||
margin: 0;
|
||||
margin: 0.2em;
|
||||
}
|
||||
|
||||
.listing-item .size,
|
||||
@@ -262,41 +262,54 @@ body.rtl .listing-items {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* These old rules are now redundant - sizing comes from unified container rules above */
|
||||
.placeholder {
|
||||
width: var(--icon-size);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.floating-window .listing-items.list .listing-item .size,
|
||||
.floating-window .listing-items.compact .listing-item .size,
|
||||
.floating-window .listing-items.list .listing-item .duration,
|
||||
.floating-window .listing-items.compact .listing-item .duration,
|
||||
.floating-window .listing-items.list .listing-item .modified,
|
||||
.floating-window .listing-items.compact .listing-item .modified {
|
||||
min-width: 20%;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
/* Item-specific styles (not header) */
|
||||
.listing-items .header .size,
|
||||
.listing-items.list .listing-item .size,
|
||||
.listing-items.compact .listing-item .size {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.listing-items .header .duration,
|
||||
.listing-items.compact .listing-item .size,
|
||||
.listing-items.list .listing-item .duration,
|
||||
.listing-items.compact .listing-item .duration {
|
||||
margin-left: auto;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.listing-items.list .listing-item .size.adjustment,
|
||||
.listing-items.compact .listing-item .size.adjustment {
|
||||
width: calc(25% + 1em) !important;
|
||||
.listing-items.compact .listing-item .duration,
|
||||
.listing-items.list .listing-item .modified,
|
||||
.listing-items.compact .listing-item .modified {
|
||||
min-width: 10%;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.listing-items.list .listing-item .name,
|
||||
.listing-items.compact .listing-item .name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.listing-items.list .listing-item .name.adjustment,
|
||||
.listing-items.compact .listing-item .name.adjustment {
|
||||
width: calc(50% + 1em) !important;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.listing-item .text {
|
||||
padding-left: 0.5em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.listing-items.icons .image-preview.download-icon {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.listing-items.icons .image-preview.download-icon .material-icons {
|
||||
font-size: 2em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.listing-items.compact .listing-item:first-child {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "الحفظ والخروج",
|
||||
"keepEditing": "استمر في التحرير",
|
||||
"closeAnyway": "إغلاق على أي حال",
|
||||
"goToItem": "الانتقال إلى العنصر",
|
||||
"watchFile": "ملف المشاهدة",
|
||||
"showMore": "إظهار الخيارات المتقدمة",
|
||||
"showLess": "إخفاء الخيارات المتقدمة"
|
||||
"showLess": "إخفاء الخيارات المتقدمة",
|
||||
"goToSource": "الانتقال إلى موقع المصدر",
|
||||
"goToItem": "انتقل إلى العنصر"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "اختر المجلد الوجهة (ثم أدخل اسم الأرشيف أدناه):",
|
||||
"archiveName": "اسم الأرشيف (archive.zip.zip الافتراضي):",
|
||||
"archiveCompression": "مستوى الضغط (0-9):",
|
||||
"archiveDeleteAfter": "حذف ملفات المصدر بعد إنشائها بنجاح",
|
||||
"archiveSuccess": "تم إنشاء الأرشيف بنجاح!",
|
||||
"unarchive": "أرشيف المقتطفات",
|
||||
"unarchiveMessage": "اختر مكان استخراج الأرشيف.",
|
||||
"unarchiveDestination": "مقتطف إلى:",
|
||||
"unarchiveDeleteAfter": "حذف الأرشيف بعد الاستخراج",
|
||||
"unarchiveSuccess": "تم استخراج الأرشيف بنجاح!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "عرض شريط الاستخدام من حجم القرص/القسم كما أبلغ عنه نظام التشغيل. من المرجح أن يكون هذا الحجم أكبر ويتضمن ملفات غير مفهرسة.",
|
||||
"usageTextDisplay": "ما هو نص الاستخدام الذي يجب عرضه؟",
|
||||
"usageTextIndexed": "الاستخدام كما هو مفهرس",
|
||||
"usageTextDisk": "الاستخدام كما تم الإبلاغ عنه بواسطة القرص/القسم"
|
||||
"usageTextDisk": "الاستخدام كما تم الإبلاغ عنه بواسطة القرص/القسم",
|
||||
"dividerNamePlaceholder": "نص المقسم (اتركه فارغًا لإظهار سطر)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "تم نسخ الملف"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "النتائج{suffix}",
|
||||
"users": "المستخدمون{suffix}",
|
||||
"photos": "الصور",
|
||||
"audio": "الصوت{suffix}",
|
||||
"audios": "ملفات صوتية{suffix}",
|
||||
"videos": "مقاطع الفيديو{suffix}",
|
||||
"models": "نماذج ثلاثية الأبعاد",
|
||||
"documents": "المستندات{suffix}",
|
||||
"archives": "المحفوظات{suffix}",
|
||||
"images": "الصور{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "الفضاء{suffix}",
|
||||
"format": "التنسيق{suffix}",
|
||||
"office": "المكتب{suffix}",
|
||||
"show": "عرض{suffix}"
|
||||
"show": "عرض{suffix}",
|
||||
"divider": "مقسم{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "قواعد الوصول",
|
||||
@@ -686,6 +688,11 @@
|
||||
"downloadChunkSizeMbHelp": "حجم القطع للتحميلات المقطعة. سيتم تنزيل الملفات الأكبر من هذا الحجم على شكل أجزاء لمنع حدوث مشكلات في المهلة (على سبيل المثال، أخطاء Cloudflare 524). يؤدي الإعداد إلى 0 إلى تعطيل التنزيلات المقطعة."
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "حذف الملفات المصدر بعد إنشاء/استخراج الأرشيفات",
|
||||
"deleteAfterArchiveDescription": "عند التمكين، سيتم حذف الملفات المصدر بعد إنشاء/استخراج الأرشيفات بنجاح",
|
||||
"hideFilesInTree": "إخفاء الملفات في شجرة التنقل",
|
||||
"hideFilesInTreeDescription": "عند التمكين، تظهر المجلدات فقط في التنقل الشجري للشريط الجانبي",
|
||||
"previewModelsDescription": "عرض صور مصغرة للنماذج ثلاثية الأبعاد، سيتم عرض أيقونة عامة إذا كانت معطلة",
|
||||
"setDateFormat": "تعيين تنسيق التاريخ الدقيق",
|
||||
"showHiddenFiles": "إظهار الملفات المخفية",
|
||||
"showHiddenFilesDescription": "إظهار الملفات المخفية في القائمة (مثل .thumbnails، ProgramData)، ولكن لا يزال يمكن الوصول إليها يدويًا إذا تم تكوينها للفهرسة.",
|
||||
@@ -737,6 +744,7 @@
|
||||
"previewFolderDescription": "إظهار معاينات الصور المصغرة للمجلدات التي تحتوي على العنصر الفرعي الأول كغلاف - أو عناصر متعددة إذا تم تمكين معاينة الحركة.",
|
||||
"previewMotion": "معاينات الحركة",
|
||||
"previewMotionVideosDescription": "معاينة الحركة هي المعاينة المنبثقة المتحركة التي تتنقل عبر إطارات الفيديو أو الصور المتعددة لمجلد",
|
||||
"previewAudiosDescription": "عرض صور مصغرة للملفات الصوتية، سيتم عرض أيقونة عامة إذا كانت معطلة",
|
||||
"popupPreview": "العارض المنبثق",
|
||||
"popupPreviewDescription": "ستظهر نافذة منبثقة متراكبة فوق واجهة المستخدم عند التمرير فوق العناصر ذات الصور المصغرة",
|
||||
"thumbnailOptions": "خيارات الصور المصغرة",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Uložit a ukončit",
|
||||
"keepEditing": "Pokračovat v úpravách",
|
||||
"closeAnyway": "Stejně zavřít",
|
||||
"goToItem": "Přejít na položku",
|
||||
"watchFile": "Sledovat soubor",
|
||||
"showMore": "Zobrazit rozšířené možnosti",
|
||||
"showLess": "Skrytí pokročilých možností"
|
||||
"showLess": "Skrytí pokročilých možností",
|
||||
"goToSource": "Přejít na zdrojové umístění",
|
||||
"goToItem": "Přejít na položku"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stáhnout soubor",
|
||||
@@ -299,12 +300,10 @@
|
||||
"archiveDestination": "Zvolte cílovou složku (pak zadejte název archivu níže):",
|
||||
"archiveName": "Název archivu (výchozí archive.zip):",
|
||||
"archiveCompression": "Úroveň komprese (0-9):",
|
||||
"archiveDeleteAfter": "Odstranění zdrojových souborů po úspěšném vytvoření",
|
||||
"archiveSuccess": "Archiv byl úspěšně vytvořen!",
|
||||
"unarchive": "Výpis archivu",
|
||||
"unarchiveMessage": "Vyberte, kam se má archiv rozbalit.",
|
||||
"unarchiveDestination": "Výpis na:",
|
||||
"unarchiveDeleteAfter": "Odstranění archivu po extrakci",
|
||||
"unarchiveSuccess": "Archiv úspěšně extrahován!"
|
||||
},
|
||||
"search": {
|
||||
@@ -323,6 +322,12 @@
|
||||
"showPreviewImagesDescription": "Zobrazení miniaturních náhledů výsledků vyhledávání, pokud jsou k dispozici, a zobrazení větších ikon u všech výsledků."
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Smazat zdrojové soubory po vytvoření/extrahování archivů",
|
||||
"deleteAfterArchiveDescription": "Pokud je povoleno, po úspěšném vytvoření/extrahování archivů dojde ke smazání zdrojových souborů",
|
||||
"hideFilesInTree": "Skrýt soubory ve stromové navigaci",
|
||||
"hideFilesInTreeDescription": "Pokud je povoleno, v postranním stromovém navigačním panelu se zobrazují pouze složky",
|
||||
"previewModelsDescription": "Zobrazit náhledy miniatur pro 3D modely, pokud je zakázáno, zobrazí se obecná ikona",
|
||||
"previewAudiosDescription": "Zobrazit náhledy miniatur pro zvuky, pokud je zakázáno, zobrazí se obecná ikona",
|
||||
"setDateFormat": "Nastavit formát data",
|
||||
"showHiddenFiles": "Zobrazit skryté soubory",
|
||||
"showQuickDownload": "Zobrazit ikonu rychlého stahování",
|
||||
@@ -517,7 +522,8 @@
|
||||
"showDiskUsageDescription": "Zobrazení sloupce využití z velikosti disku/oddílu podle hlášení operačního systému. Tato velikost bude pravděpodobně větší a zahrnuje i soubory, které nejsou indexovány.",
|
||||
"usageTextDisplay": "Který text použití se má zobrazit?",
|
||||
"usageTextIndexed": "použití podle indexu",
|
||||
"usageTextDisk": "využití podle hlášení disku/oddílu"
|
||||
"usageTextDisk": "využití podle hlášení disku/oddílu",
|
||||
"dividerNamePlaceholder": "Text oddělovače (pro zobrazení řádku nechte prázdný)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Odkaz zkopírován!"
|
||||
@@ -642,8 +648,9 @@
|
||||
"results": "Výsledky{suffix}",
|
||||
"users": "Uživatelé{suffix}",
|
||||
"photos": "Fotografie",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Zvuky{suffix}",
|
||||
"videos": "Videa{suffix}",
|
||||
"models": "3D modely",
|
||||
"documents": "Dokumenty{suffix}",
|
||||
"archives": "Archivy{suffix}",
|
||||
"images": "Obrázky{suffix}",
|
||||
@@ -726,7 +733,8 @@
|
||||
"space": "Prostor{suffix}",
|
||||
"format": "Formát{suffix}",
|
||||
"office": "Kancelář{suffix}",
|
||||
"show": "Zobrazit{suffix}"
|
||||
"show": "Zobrazit{suffix}",
|
||||
"divider": "Rozdělovač{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Pravidla přístupu",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Speichern und Beenden",
|
||||
"keepEditing": "Weiter bearbeiten",
|
||||
"closeAnyway": "Jedenfalls schließen",
|
||||
"goToItem": "Weiter zu Artikel",
|
||||
"watchFile": "Datei beobachten",
|
||||
"showMore": "Erweiterte Optionen anzeigen",
|
||||
"showLess": "Erweiterte Optionen ausblenden"
|
||||
"showLess": "Erweiterte Optionen ausblenden",
|
||||
"goToSource": "Zum Quellort gehen",
|
||||
"goToItem": "Weiter zum Artikel"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Datei herunterladen",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Wählen Sie den Zielordner (geben Sie dann unten den Archivnamen ein):",
|
||||
"archiveName": "Name des Archivs (standardmäßig archive.zip):",
|
||||
"archiveCompression": "Komprimierungsstufe (0-9):",
|
||||
"archiveDeleteAfter": "Quelldateien nach erfolgreicher Erstellung löschen",
|
||||
"archiveSuccess": "Archiv erfolgreich erstellt!",
|
||||
"unarchive": "Archiv extrahieren",
|
||||
"unarchiveMessage": "Wählen Sie, wohin das Archiv entpackt werden soll.",
|
||||
"unarchiveDestination": "Auszug zu:",
|
||||
"unarchiveDeleteAfter": "Archiv nach Extraktion löschen",
|
||||
"unarchiveSuccess": "Archiv erfolgreich entpackt!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Anzeige der Nutzungsleiste anhand der vom Betriebssystem gemeldeten Festplatten-/Partitionsgröße. Diese Größe wird wahrscheinlich größer sein und schließt nicht indizierte Dateien ein.",
|
||||
"usageTextDisplay": "Welcher Verwendungstext soll angezeigt werden?",
|
||||
"usageTextIndexed": "Nutzung wie indiziert",
|
||||
"usageTextDisk": "Nutzung, wie von der Festplatte/Partition gemeldet"
|
||||
"usageTextDisk": "Nutzung, wie von der Festplatte/Partition gemeldet",
|
||||
"dividerNamePlaceholder": "Teilertext (leer lassen, um eine Zeile anzuzeigen)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link wurde kopiert!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Ergebnisse{suffix}",
|
||||
"users": "Benutzer{suffix}",
|
||||
"photos": "Fotos",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Audios{suffix}",
|
||||
"videos": "Videos{suffix}",
|
||||
"models": "3D-Modelle",
|
||||
"documents": "Dokumente{suffix}",
|
||||
"archives": "Archiv{suffix}",
|
||||
"images": "Bilder{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Weltraum{suffix}",
|
||||
"format": "Format{suffix}",
|
||||
"office": "Büro{suffix}",
|
||||
"show": "anzeigen{suffix}"
|
||||
"show": "anzeigen{suffix}",
|
||||
"divider": "Teiler{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Zugangsregeln",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "orange"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Quelldateien nach Erstellung/Extraktion von Archiven löschen",
|
||||
"deleteAfterArchiveDescription": "Wenn aktiviert, werden die Quelldateien nach erfolgreicher Erstellung/Extraktion von Archiven gelöscht",
|
||||
"hideFilesInTree": "Dateien in der Baumnavigation ausblenden",
|
||||
"hideFilesInTreeDescription": "Wenn aktiviert, werden in der Seitenleisten-Baumnavigation nur Ordner angezeigt",
|
||||
"previewModelsDescription": "Vorschaubilder für 3D-Modelle anzeigen, bei Deaktivierung wird ein generisches Symbol angezeigt",
|
||||
"previewAudiosDescription": "Vorschaubilder für Audiodateien anzeigen, bei Deaktivierung wird ein generisches Symbol angezeigt",
|
||||
"setDateFormat": "Genaues Datum statt relativer Zeitangaben verwenden",
|
||||
"showHiddenFiles": "Versteckte Dateien anzeigen",
|
||||
"showHiddenFilesDescription": "Versteckte Dateien in der Auflistung anzeigen (z.B. .thumbnails, ProgramData), aber sie sind immer noch manuell zugänglich, wenn sie für die Indizierung konfiguriert sind.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Αποθήκευση και έξοδος",
|
||||
"keepEditing": "Συνεχίστε την επεξεργασία",
|
||||
"closeAnyway": "Κλείστε Ούτως ή άλλως",
|
||||
"goToItem": "Πηγαίνετε στο στοιχείο",
|
||||
"watchFile": "Παρακολουθήστε το αρχείο",
|
||||
"showMore": "Εμφάνιση προηγμένων επιλογών",
|
||||
"showLess": "Απόκρυψη προηγμένων επιλογών"
|
||||
"showLess": "Απόκρυψη προηγμένων επιλογών",
|
||||
"goToSource": "Πηγαίνετε στην τοποθεσία της πηγής",
|
||||
"goToItem": "Πηγαίνετε στο στοιχείο"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Λήψη αρχείου",
|
||||
@@ -224,12 +225,10 @@
|
||||
"archiveDestination": "Επιλέξτε φάκελο προορισμού (στη συνέχεια, εισάγετε το όνομα του αρχείου παρακάτω):",
|
||||
"archiveName": "Όνομα αρχείου (προεπιλεγμένο archive.zip):",
|
||||
"archiveCompression": "Επίπεδο συμπίεσης (0-9):",
|
||||
"archiveDeleteAfter": "Διαγραφή αρχείων πηγής μετά την επιτυχή δημιουργία",
|
||||
"archiveSuccess": "Το αρχείο δημιουργήθηκε με επιτυχία!",
|
||||
"unarchive": "Απόσπασμα αρχείου",
|
||||
"unarchiveMessage": "Επιλέξτε πού θα εξαγάγετε το αρχείο.",
|
||||
"unarchiveDestination": "Απόσπασμα προς:",
|
||||
"unarchiveDeleteAfter": "Διαγραφή αρχείου μετά την εξαγωγή",
|
||||
"unarchiveSuccess": "Το αρχείο εξήχθη με επιτυχία!"
|
||||
},
|
||||
"search": {
|
||||
@@ -384,7 +383,8 @@
|
||||
"showDiskUsageDescription": "Εμφάνιση μπάρας χρήσης από το μέγεθος του δίσκου/κατάτμησης όπως αναφέρεται από το λειτουργικό σύστημα. Αυτό το μέγεθος είναι πιθανό να είναι μεγαλύτερο και περιλαμβάνει αρχεία που δεν έχουν ευρετηριαστεί.",
|
||||
"usageTextDisplay": "Ποιο κείμενο χρήσης πρέπει να εμφανίζεται;",
|
||||
"usageTextIndexed": "χρήση ως ευρετήριο",
|
||||
"usageTextDisk": "χρήση όπως αναφέρεται από δίσκο/κατάτμηση"
|
||||
"usageTextDisk": "χρήση όπως αναφέρεται από δίσκο/κατάτμηση",
|
||||
"dividerNamePlaceholder": "Κείμενο διαχωριστικού (αφήστε το κενό για να εμφανιστεί μια γραμμή)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Αποτελέσματα{suffix}",
|
||||
"users": "Χρήστες{suffix}",
|
||||
"photos": "Φωτογραφίες",
|
||||
"audio": "Ήχος{suffix}",
|
||||
"audios": "Ήχοι{suffix}",
|
||||
"videos": "Βίντεο{suffix}",
|
||||
"models": "Τρισδιάστατα μοντέλα",
|
||||
"documents": "Έγγραφα{suffix}",
|
||||
"archives": "Αρχεία{suffix}",
|
||||
"images": "Εικόνες{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Διάστημα{suffix}",
|
||||
"format": "Μορφή{suffix}",
|
||||
"office": "Γραφείο{suffix}",
|
||||
"show": "Εμφάνιση{suffix}"
|
||||
"show": "Εμφάνιση{suffix}",
|
||||
"divider": "Διαχωριστικό{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Κανόνες πρόσβασης",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "πορτοκαλί"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Διαγραφή αρχείων προέλευσης μετά τη δημιουργία/εξαγωγή αρχείων",
|
||||
"deleteAfterArchiveDescription": "Όταν ενεργοποιηθεί, θα διαγραφούν τα αρχεία προέλευσης μετά την επιτυχή δημιουργία/εξαγωγή αρχείων",
|
||||
"hideFilesInTree": "Απόκρυψη αρχείων στην πλοήγηση δέντρου",
|
||||
"hideFilesInTreeDescription": "Όταν ενεργοποιηθεί, μόνο οι φάκελοι εμφανίζονται στην πλευρική δενδρική πλοήγηση",
|
||||
"previewModelsDescription": "Εμφάνιση μικρογραφιών για τρισδιάστατα μοντέλα, θα εμφανιστεί γενικό εικονίδιο εάν είναι απενεργοποιημένο",
|
||||
"previewAudiosDescription": "Εμφάνιση μικρογραφιών για ήχους, θα εμφανιστεί γενικό εικονίδιο εάν είναι απενεργοποιημένο",
|
||||
"setDateFormat": "Ορισμός ακριβούς μορφής ημερομηνίας",
|
||||
"showHiddenFiles": "Εμφάνιση κρυφών αρχείων",
|
||||
"showHiddenFilesDescription": "Εμφάνιση κρυμμένων αρχείων στην καταχώριση (π.χ. .thumbnails, ProgramData), αλλά εξακολουθούν να είναι προσβάσιμα χειροκίνητα, εάν έχουν ρυθμιστεί για ευρετηρίαση.",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"incompleteResultsDetails": "Showing max results ({max}). Try narrowing your search to get more results under {max}."
|
||||
},
|
||||
"general": {
|
||||
"divider": "Divider{suffix}",
|
||||
"show": "Show{suffix}",
|
||||
"office": "Office{suffix}",
|
||||
"format": "Format{suffix}",
|
||||
@@ -56,12 +57,13 @@
|
||||
"scopes": "Scopes{suffix}",
|
||||
"commands": "Commands{suffix}",
|
||||
"photos": "Photos",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Audios{suffix}",
|
||||
"videos": "Videos{suffix}",
|
||||
"documents": "Documents{suffix}",
|
||||
"archives": "Archives{suffix}",
|
||||
"images": "Images{suffix}",
|
||||
"music": "Music{suffix}",
|
||||
"models": "3D Models",
|
||||
"pdf": "PDF{suffix}",
|
||||
"search": "Search{suffix}",
|
||||
"number": "Number{suffix}",
|
||||
@@ -160,7 +162,8 @@
|
||||
"saveAndExit": "Save and Exit",
|
||||
"keepEditing": "Keep Editing",
|
||||
"closeAnyway": "Close Anyways",
|
||||
"goToItem": "Go to Item"
|
||||
"goToItem": "Go to item",
|
||||
"goToSource": "Go to Source Location"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -365,12 +368,10 @@
|
||||
"archiveDestination": "Choose destination folder (then enter archive name below):",
|
||||
"archiveName": "Archive name (default archive.zip):",
|
||||
"archiveCompression": "Compression level (0–9):",
|
||||
"archiveDeleteAfter": "Delete source files after successful creation",
|
||||
"archiveSuccess": "Archive created successfully!",
|
||||
"unarchive": "Extract archive",
|
||||
"unarchiveMessage": "Choose where to extract the archive.",
|
||||
"unarchiveDestination": "Extract to:",
|
||||
"unarchiveDeleteAfter": "Delete archive after successful decompression",
|
||||
"unarchiveSuccess": "Archive extracted successfully!",
|
||||
"dragAndDrop": "Drag and drop files here",
|
||||
"error": "Something went wrong",
|
||||
@@ -452,6 +453,10 @@
|
||||
"helpText2": "The index: Search utilizes the index which automatically gets updated on the configured interval (default: 5 minutes). Searching when the program has just started may result in incomplete results."
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Delete source files after creation/extraction of archives",
|
||||
"deleteAfterArchiveDescription": "When enabled, will delete the source files after successful creation/extraction of archives",
|
||||
"hideFilesInTree": "Hide files in tree navigation",
|
||||
"hideFilesInTreeDescription": "When enabled, only folders are shown in the sidebar Tree Navigation.",
|
||||
"previewFolder": "Show thumbnails for folders if available",
|
||||
"previewFolderDescription": "Show thumbnail previews for folders with first child item as cover -- or multiple items if motion preview is enabled.",
|
||||
"debugOfficeEditor": "Enable OnlyOffice Debug Mode",
|
||||
@@ -469,6 +474,8 @@
|
||||
"showQuickDownloadDescription": "Show download icon for quick access on each file item that can be clicked to download instantly.",
|
||||
"previewImagesDescription": "Show thumbnail previews for images, will show generic icon if disabled.",
|
||||
"previewVideosDescription": "Show thumbnail previews for videos, will show generic icon if disabled.",
|
||||
"previewAudiosDescription": "Show thumbnail previews for audios, will show generic icon if disabled.",
|
||||
"previewModelsDescription": "Show thumbnail previews for 3D Models, will show generic icon if disabled.",
|
||||
"previewMotion": "Motion previews",
|
||||
"previewMotionVideosDescription": "Motion preview is the animated popup preview that cycles through the frames of the video or multiple images for a folder",
|
||||
"editOffice": "View and edit office files with OnlyOffice",
|
||||
@@ -604,6 +611,7 @@
|
||||
"adminOptions": "Admin user options"
|
||||
},
|
||||
"sidebar": {
|
||||
"dividerNamePlaceholder": "Divider text (leave blank to show a line)",
|
||||
"siteSettings": "Site Settings",
|
||||
"updateIsAvailable": "An update is available!",
|
||||
"fileActions": "File actions",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Guardar y salir",
|
||||
"keepEditing": "Seguir editando",
|
||||
"closeAnyway": "Cerrar de todos modos",
|
||||
"goToItem": "Ir al elemento",
|
||||
"watchFile": "Ver archivo",
|
||||
"showMore": "Mostrar opciones avanzadas",
|
||||
"showLess": "Ocultar opciones avanzadas"
|
||||
"showLess": "Ocultar opciones avanzadas",
|
||||
"goToSource": "Ir a la ubicación de origen",
|
||||
"goToItem": "Ir al artículo"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descargar archivo",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Elija la carpeta de destino (a continuación, introduzca el nombre del archivo):",
|
||||
"archiveName": "Nombre del archivo (por defecto archive.zip):",
|
||||
"archiveCompression": "Nivel de compresión (0-9):",
|
||||
"archiveDeleteAfter": "Eliminar los archivos de origen después de crearlos correctamente",
|
||||
"archiveSuccess": "¡Archivo creado con éxito!",
|
||||
"unarchive": "Extraer archivo",
|
||||
"unarchiveMessage": "Elija dónde extraer el archivo.",
|
||||
"unarchiveDestination": "Extraer a:",
|
||||
"unarchiveDeleteAfter": "Eliminar el archivo tras la extracción",
|
||||
"unarchiveSuccess": "¡Archivo extraído con éxito!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Muestra la barra de uso a partir del tamaño del disco/partición indicado por el sistema operativo. Es probable que este tamaño sea mayor e incluya archivos no indexados.",
|
||||
"usageTextDisplay": "¿Qué texto de uso debe mostrarse?",
|
||||
"usageTextIndexed": "uso como indexado",
|
||||
"usageTextDisk": "uso por disco/partición"
|
||||
"usageTextDisk": "uso por disco/partición",
|
||||
"dividerNamePlaceholder": "Texto divisor (déjelo en blanco para mostrar una línea)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "¡Enlace copiado!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Resultados{suffix}",
|
||||
"users": "Usuarios{suffix}",
|
||||
"photos": "Fotos",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Audios{suffix}",
|
||||
"videos": "Vídeos{suffix}",
|
||||
"models": "Modelos 3D",
|
||||
"documents": "Documentos{suffix}",
|
||||
"archives": "Archivos comprimidos{suffix}",
|
||||
"images": "Imágenes{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Espacio{suffix}",
|
||||
"format": "Formato{suffix}",
|
||||
"office": "Oficina{suffix}",
|
||||
"show": "Mostrar{suffix}"
|
||||
"show": "Mostrar{suffix}",
|
||||
"divider": "Divisor{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Reglas de acceso",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "Naranja"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Eliminar archivos originales después de la creación/extracción de archivos",
|
||||
"deleteAfterArchiveDescription": "Cuando está activado, eliminará los archivos originales después de la creación o extracción de archivos",
|
||||
"hideFilesInTree": "Ocultar archivos en la navegación de árbol",
|
||||
"hideFilesInTreeDescription": "Cuando está activado, solo se muestrarán carpetas en la navegación del panel lateral",
|
||||
"previewModelsDescription": "Mostrar vistas previas en miniatura para modelos 3D, se mostrará un icono genérico si está desactivado",
|
||||
"previewAudiosDescription": "Mostrar vistas previas en miniatura para audios, si está desactivado mostrará un icono generico",
|
||||
"autoplayMedia": "Reproducción automática de archivos multimedia",
|
||||
"autoplayMediaDescription": "Reproduce automaticamente los archivos multimedia al abrirlos",
|
||||
"sidebarOptions": "Opciones del panel lateral",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Sauvegarder et quitter",
|
||||
"keepEditing": "Continuer à éditer",
|
||||
"closeAnyway": "Fermer Quoi qu'il en soit",
|
||||
"goToItem": "Aller à l'article",
|
||||
"watchFile": "Regarder le dossier",
|
||||
"showMore": "Afficher les options avancées",
|
||||
"showLess": "Masquer les options avancées"
|
||||
"showLess": "Masquer les options avancées",
|
||||
"goToSource": "Aller à l'emplacement de la source",
|
||||
"goToItem": "Aller à l'article"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Télécharger le fichier",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Choisissez le dossier de destination (puis entrez le nom de l'archive ci-dessous) :",
|
||||
"archiveName": "Nom de l'archive (par défaut archive.zip) :",
|
||||
"archiveCompression": "Niveau de compression (0-9) :",
|
||||
"archiveDeleteAfter": "Supprimer les fichiers sources après une création réussie",
|
||||
"archiveSuccess": "Archive créée avec succès !",
|
||||
"unarchive": "Extraire l'archive",
|
||||
"unarchiveMessage": "Choisissez l'endroit où extraire l'archive.",
|
||||
"unarchiveDestination": "Extrait à :",
|
||||
"unarchiveDeleteAfter": "Supprimer l'archive après l'extraction",
|
||||
"unarchiveSuccess": "L'archive a été extraite avec succès !"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Affichage de la barre d'utilisation à partir de la taille du disque/de la partition telle qu'elle est indiquée par le système d'exploitation. Cette taille est probablement plus importante et inclut les fichiers non indexés.",
|
||||
"usageTextDisplay": "Quel texte d'utilisation doit être affiché ?",
|
||||
"usageTextIndexed": "utilisation telle qu'indexée",
|
||||
"usageTextDisk": "utilisation telle que rapportée par disque/partition"
|
||||
"usageTextDisk": "utilisation telle que rapportée par disque/partition",
|
||||
"dividerNamePlaceholder": "Texte du séparateur (laisser en blanc pour montrer une ligne)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Lien copié !"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Résultats{suffix}",
|
||||
"users": "Utilisateurs{suffix}",
|
||||
"photos": "Photos",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Audios{suffix}",
|
||||
"videos": "Vidéos{suffix}",
|
||||
"models": "Modèles 3D",
|
||||
"documents": "Documents{suffix}",
|
||||
"archives": "Archives{suffix}",
|
||||
"images": "Images{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "L'espace{suffix}",
|
||||
"format": "Format{suffix}",
|
||||
"office": "Bureau{suffix}",
|
||||
"show": "Afficher{suffix}"
|
||||
"show": "Afficher{suffix}",
|
||||
"divider": "Diviseur{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Règles d'accès",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "orange"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Supprimer les fichiers source après la création/extraction d'archives",
|
||||
"deleteAfterArchiveDescription": "Lorsque activé, supprimera les fichiers source après la création/extraction réussie d'archives",
|
||||
"hideFilesInTree": "Masquer les fichiers dans l'arborescence",
|
||||
"hideFilesInTreeDescription": "Lorsque activé, seuls les dossiers sont affichés dans l'arborescence de navigation de la barre latérale",
|
||||
"previewModelsDescription": "Afficher les aperçus miniatures pour les modèles 3D, une icône générique s'affichera si désactivé",
|
||||
"previewAudiosDescription": "Afficher les aperçus miniatures pour les fichiers audio, une icône générique s'affichera si désactivé",
|
||||
"setDateFormat": "Définir le format exact de la date",
|
||||
"showHiddenFiles": "Afficher les fichiers cachés",
|
||||
"showHiddenFilesDescription": "Affiche les fichiers cachés dans la liste (par exemple, .thumbnails, ProgramData), mais ils restent accessibles manuellement s'ils sont configurés pour être indexés.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "שמור וצא",
|
||||
"keepEditing": "המשך עריכה",
|
||||
"closeAnyway": "סגור בכל מקרה",
|
||||
"goToItem": "עבור לפריט",
|
||||
"watchFile": "צפה בקובץ",
|
||||
"showMore": "הצג אפשרויות מתקדמות",
|
||||
"showLess": "הסתר אפשרויות מתקדמות"
|
||||
"showLess": "הסתר אפשרויות מתקדמות",
|
||||
"goToSource": "עבור למיקום המקור",
|
||||
"goToItem": "עבור לפריט"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "הורד קובץ",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "בחר תיקיית יעד (ואז הזן את שם הארכיון למטה):",
|
||||
"archiveName": "שם הארכיון (ברירת מחדל: archive.zip):",
|
||||
"archiveCompression": "רמת דחיסה (0–9):",
|
||||
"archiveDeleteAfter": "מחק קבצי מקור לאחר יצירה מוצלחת",
|
||||
"archiveSuccess": "הארכיון נוצר בהצלחה!",
|
||||
"unarchive": "חלץ ארכיון",
|
||||
"unarchiveMessage": "בחר היכן לחלץ את הארכיון.",
|
||||
"unarchiveDestination": "חלץ אל:",
|
||||
"unarchiveDeleteAfter": "מחק את הארכיון לאחר החילוץ",
|
||||
"unarchiveSuccess": "הארכיון חולץ בהצלחה!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "הצג את סרגל השימוש בגודל הדיסק/מחיצה כפי שדווח על ידי מערכת ההפעלה. גודל זה צפוי להיות גדול יותר וכולל קבצים שלא הוכנסו לאינדקס.",
|
||||
"usageTextDisplay": "איזה טקסט שימוש יש להציג?",
|
||||
"usageTextIndexed": "שימוש כפי שמופיע באינדקס",
|
||||
"usageTextDisk": "שימוש כפי שדווח על ידי דיסק/מחיצה"
|
||||
"usageTextDisk": "שימוש כפי שדווח על ידי דיסק/מחיצה",
|
||||
"dividerNamePlaceholder": "טקסט מפריד (השאר ריק כדי להציג קו)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "הקישור הועתק!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "תוצאות{suffix}",
|
||||
"users": "משתמשים{suffix}",
|
||||
"photos": "תמונות",
|
||||
"audio": "אודיו{suffix}",
|
||||
"audios": "שמע{suffix}",
|
||||
"videos": "סרטונים{suffix}",
|
||||
"models": "דגמי תלת-ממד",
|
||||
"documents": "מסמכים{suffix}",
|
||||
"archives": "ארכיונים{suffix}",
|
||||
"images": "תמונות{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "חלל{suffix}",
|
||||
"format": "פורמט{suffix}",
|
||||
"office": "משרד{suffix}",
|
||||
"show": "הצג{suffix}"
|
||||
"show": "הצג{suffix}",
|
||||
"divider": "מפריד{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "כללי גישה",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "כתום"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "מחק קבצי מקור לאחר יצירה/חילוץ של ארכיונים",
|
||||
"deleteAfterArchiveDescription": "כאשר מופעל, ימחק את קבצי המקור לאחר יצירה/חילוץ מוצלחים של ארכיונים",
|
||||
"hideFilesInTree": "הסתר קבצים בניווט עץ",
|
||||
"hideFilesInTreeDescription": "כאשר מופעל, רק תיקיות מוצגות בניווט העץ של סרגל הצד",
|
||||
"previewModelsDescription": "הצג תצוגות מקדימות ממוזערות לדגמי תלת-ממד, יוצג אייקון כללי אם מושבת",
|
||||
"previewAudiosDescription": "הצג תצוגות מקדימות ממוזערות לשמע, יוצג אייקון כללי אם מושבת",
|
||||
"setDateFormat": "הגדר פורמט תאריך מדויק",
|
||||
"showHiddenFiles": "הצג קבצים מוסתרים",
|
||||
"showHiddenFilesDescription": "הצג קבצים מוסתרים ברשימה (לדוגמה, .thumbnails, ProgramData), אך הם עדיין נגישים באופן ידני אם הוגדרו לאינדקס.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Mentés és kilépés",
|
||||
"keepEditing": "Szerkesztés folytatása",
|
||||
"closeAnyway": "Egyébként bezárni",
|
||||
"goToItem": "Tovább a tételhez",
|
||||
"watchFile": "Nézze meg a fájlt",
|
||||
"showMore": "Speciális beállítások megjelenítése",
|
||||
"showLess": "Speciális beállítások elrejtése"
|
||||
"showLess": "Speciális beállítások elrejtése",
|
||||
"goToSource": "Menj a forráshelyre",
|
||||
"goToItem": "Tovább a tételhez"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Fájl letöltése",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Válassza ki a célmappát (majd adja meg az archívum nevét):",
|
||||
"archiveName": "Archívum neve (alapértelmezett archive.zip):",
|
||||
"archiveCompression": "Tömörítési szint (0-9):",
|
||||
"archiveDeleteAfter": "Forrásfájlok törlése sikeres létrehozás után",
|
||||
"archiveSuccess": "Az archívum sikeresen létrehozva!",
|
||||
"unarchive": "Kivonat archívum",
|
||||
"unarchiveMessage": "Válassza ki, hová szeretné kinyomtatni az archívumot.",
|
||||
"unarchiveDestination": "Kivonat a következő címre:",
|
||||
"unarchiveDeleteAfter": "Törölje az archívumot az extrakció után",
|
||||
"unarchiveSuccess": "Az archívum sikeresen kinyomtatva!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "A lemez/partíció méretéből származó, az operációs rendszer által jelentett használati sáv megjelenítése. Ez a méret valószínűleg nagyobb lesz, és tartalmazza az indexeletlen fájlokat is.",
|
||||
"usageTextDisplay": "Milyen használati szöveget kell megjeleníteni?",
|
||||
"usageTextIndexed": "indexált használat",
|
||||
"usageTextDisk": "lemez/partíció által bejelentett használat"
|
||||
"usageTextDisk": "lemez/partíció által bejelentett használat",
|
||||
"dividerNamePlaceholder": "Választószöveg (üresen hagyva egy sor megjelenítéséhez)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link másolva!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Eredmények{suffix}",
|
||||
"users": "Felhasználók{suffix}",
|
||||
"photos": "Fotók",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Hangfájlok{suffix}",
|
||||
"videos": "Videók{suffix}",
|
||||
"models": "3D modellek",
|
||||
"documents": "Dokumentumok{suffix}",
|
||||
"archives": "Archívum{suffix}",
|
||||
"images": "Képek{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Űr{suffix}",
|
||||
"format": "Formátum{suffix}",
|
||||
"office": "Iroda{suffix}",
|
||||
"show": "Mutasd meg a{suffix}"
|
||||
"show": "Mutasd meg a{suffix}",
|
||||
"divider": "Választó{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Hozzáférési szabályok",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "narancs"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Forrásfájlok törlése archívumok létrehozása/kibontása után",
|
||||
"deleteAfterArchiveDescription": "Ha engedélyezve van, törli a forrásfájlokat az archívumok sikeres létrehozása/kibontása után",
|
||||
"hideFilesInTree": "Fájlok elrejtése a fa navigációban",
|
||||
"hideFilesInTreeDescription": "Ha engedélyezve van, csak mappák jelennek meg az oldalsáv fa navigációjában",
|
||||
"previewModelsDescription": "Miniatűr előnézetek megjelenítése 3D modellekhez, általános ikon jelenik meg ha letiltott",
|
||||
"previewAudiosDescription": "Miniatűr előnézetek megjelenítése hangfájlokhoz, általános ikon jelenik meg ha letiltott",
|
||||
"setDateFormat": "Pontos dátumformátum beállítása",
|
||||
"showHiddenFiles": "Rejtett fájlok megjelenítése",
|
||||
"showHiddenFilesDescription": "Rejtett fájlok megjelenítése a listában (pl. .thumbnails, ProgramData), de kézzel továbbra is elérhetőek, ha indexelésre vannak beállítva.",
|
||||
|
||||
@@ -274,8 +274,9 @@
|
||||
"onlyFiles": "Aðeins Skrár",
|
||||
"showOptions": "Sýna Valmöguleika",
|
||||
"photos": "Myndir",
|
||||
"audio": "Hljóð",
|
||||
"audios": "Hljóðskrár{suffix}",
|
||||
"videos": "Myndskeið",
|
||||
"models": "Þrívíð líkön",
|
||||
"documents": "Skjöl",
|
||||
"archives": "Safnskrár",
|
||||
"notEnoughCharacters": "Ekki nógu mörg stafir til að leita (lágmark {minSearchLength})",
|
||||
@@ -295,6 +296,12 @@
|
||||
"helpText2": "Vísirinn: Leit nýtir vísinn sem uppfærast sjálfkrafa á stilluðu millibili (sjálfgefið: 5 mínútur). Leit þegar forritið er nýrætt getur leitt til ófullnægjandi niðurstaðna."
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Eyða upprunaskrám eftir stofnun/útdrátt safnskráa",
|
||||
"deleteAfterArchiveDescription": "Þegar virkt, mun eyða upprunaskrám eftir árangursríka stofnun/útdrátt safnskráa",
|
||||
"hideFilesInTree": "Fela skrár í trjáflakki",
|
||||
"hideFilesInTreeDescription": "Þegar virkt, eru aðeins möppur sýndar í hliðarstiku trjáflakki",
|
||||
"previewModelsDescription": "Sýna smámyndaforskoðun fyrir þrívíð líkön, mun sýna almenn tákn ef óvirkt",
|
||||
"previewAudiosDescription": "Sýna smámyndaforskoðun fyrir hljóðskrár, mun sýna almenn tákn ef óvirkt",
|
||||
"previewFolder": "Sýna smámyndir fyrir möppur ef tiltækar",
|
||||
"previewFolderDescription": "Sýna smámyndaforskoðun fyrir möppur með fyrsta barnatriði með smámynd sem forsíðu.",
|
||||
"debugOfficeEditor": "Virkja OnlyOffice Kembunarbil",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Salvare e uscire",
|
||||
"keepEditing": "Continuare a modificare",
|
||||
"closeAnyway": "Chiudere comunque",
|
||||
"goToItem": "Vai all'articolo",
|
||||
"watchFile": "Guarda il file",
|
||||
"showMore": "Mostra opzioni avanzate",
|
||||
"showLess": "Nascondere le opzioni avanzate"
|
||||
"showLess": "Nascondere le opzioni avanzate",
|
||||
"goToSource": "Vai alla posizione della fonte",
|
||||
"goToItem": "Vai alla voce"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Scarica file",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Scegliere la cartella di destinazione (quindi inserire il nome dell'archivio sotto):",
|
||||
"archiveName": "Nome dell'archivio (predefinito archive.zip):",
|
||||
"archiveCompression": "Livello di compressione (0-9):",
|
||||
"archiveDeleteAfter": "Cancellare i file sorgente dopo la creazione",
|
||||
"archiveSuccess": "Archivio creato con successo!",
|
||||
"unarchive": "Estrarre l'archivio",
|
||||
"unarchiveMessage": "Scegliere dove estrarre l'archivio.",
|
||||
"unarchiveDestination": "Estratto a:",
|
||||
"unarchiveDeleteAfter": "Eliminare l'archivio dopo l'estrazione",
|
||||
"unarchiveSuccess": "Archivio estratto con successo!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Visualizza la barra di utilizzo dalla dimensione del disco/partizione come riportata dal sistema operativo. Questa dimensione sarà probabilmente maggiore e comprende i file non indicizzati.",
|
||||
"usageTextDisplay": "Quale testo d'uso deve essere visualizzato?",
|
||||
"usageTextIndexed": "utilizzo come indicizzato",
|
||||
"usageTextDisk": "utilizzo come riportato dal disco/partizione"
|
||||
"usageTextDisk": "utilizzo come riportato dal disco/partizione",
|
||||
"dividerNamePlaceholder": "Testo del divisore (lasciare vuoto per mostrare una riga)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link copiato!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Risultati{suffix}",
|
||||
"users": "Utenti{suffix}",
|
||||
"photos": "Foto",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Audios{suffix}",
|
||||
"videos": "Video{suffix}",
|
||||
"models": "Modelli 3D",
|
||||
"documents": "Documenti{suffix}",
|
||||
"archives": "Archivio{suffix}",
|
||||
"images": "Immagini{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Spazio{suffix}",
|
||||
"format": "Formato{suffix}",
|
||||
"office": "Ufficio{suffix}",
|
||||
"show": "Mostra{suffix}"
|
||||
"show": "Mostra{suffix}",
|
||||
"divider": "Divisore{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regole di accesso",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "arancione"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Elimina i file sorgente dopo la creazione/estrazione di archivi",
|
||||
"deleteAfterArchiveDescription": "Quando abilitato, eliminerà i file sorgente dopo la creazione/estrazione riuscita di archivi",
|
||||
"hideFilesInTree": "Nascondi file nella navigazione ad albero",
|
||||
"hideFilesInTreeDescription": "Quando abilitato, solo le cartelle vengono mostrate nella navigazione ad albero della barra laterale",
|
||||
"previewModelsDescription": "Mostra anteprime in miniatura per modelli 3D, verrà mostrata un'icona generica se disabilitato",
|
||||
"previewAudiosDescription": "Mostra anteprime in miniatura per gli audio, verrà mostrata un'icona generica se disabilitato",
|
||||
"setDateFormat": "Impostare il formato esatto della data",
|
||||
"showHiddenFiles": "Mostra i file nascosti",
|
||||
"showHiddenFilesDescription": "Mostra i file nascosti nell'elenco (ad esempio .thumbnails, ProgramData), ma sono ancora accessibili manualmente se configurati per essere indicizzati.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "保存して終了する",
|
||||
"keepEditing": "編集を続ける",
|
||||
"closeAnyway": "とにかく閉じる",
|
||||
"goToItem": "アイテムへ",
|
||||
"watchFile": "ウォッチファイル",
|
||||
"showMore": "詳細オプションを表示",
|
||||
"showLess": "詳細オプションを隠す"
|
||||
"showLess": "詳細オプションを隠す",
|
||||
"goToSource": "ソース・ロケーションへ",
|
||||
"goToItem": "アイテムへ"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "ファイルをダウンロード",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "保存先フォルダを選択します:",
|
||||
"archiveName": "アーカイブ名(デフォルトはarchive.zip):",
|
||||
"archiveCompression": "圧縮レベル(0-9):",
|
||||
"archiveDeleteAfter": "作成に成功したらソースファイルを削除する",
|
||||
"archiveSuccess": "アーカイブは正常に作成されました!",
|
||||
"unarchive": "アーカイブの展開",
|
||||
"unarchiveMessage": "アーカイブを展開する場所を選択します。",
|
||||
"unarchiveDestination": "に抜粋する:",
|
||||
"unarchiveDeleteAfter": "抽出後にアーカイブを削除する",
|
||||
"unarchiveSuccess": "アーカイブは正常に抽出されました!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "OSが報告したディスク/パーティションサイズから使用量バーを表示します。このサイズは、インデックス化されていないファイルを含むため、より大きくなる可能性があります。",
|
||||
"usageTextDisplay": "どのテキストを表示するべきか?",
|
||||
"usageTextIndexed": "索引どおり",
|
||||
"usageTextDisk": "ディスク/パーティションごとの使用量"
|
||||
"usageTextDisk": "ディスク/パーティションごとの使用量",
|
||||
"dividerNamePlaceholder": "分割テキスト(行を表示するには空白にする)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "リンクがコピーされました!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "結果{suffix}",
|
||||
"users": "ユーザー{suffix}",
|
||||
"photos": "写真",
|
||||
"audio": "オーディオ{suffix}",
|
||||
"audios": "オーディオ{suffix}",
|
||||
"videos": "ビデオ{suffix}",
|
||||
"models": "3Dモデル",
|
||||
"documents": "資料{suffix}",
|
||||
"archives": "アーカイブス{suffix}",
|
||||
"images": "画像{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "スペース{suffix}",
|
||||
"format": "フォーマット{suffix}",
|
||||
"office": "オフィス{suffix}",
|
||||
"show": "ショー{suffix}"
|
||||
"show": "ショー{suffix}",
|
||||
"divider": "ディバイダー{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "アクセスルール",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "オレンジ"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "アーカイブの作成/展開後にソースファイルを削除",
|
||||
"deleteAfterArchiveDescription": "有効にすると、アーカイブの作成/展開が成功した後にソースファイルを削除します",
|
||||
"hideFilesInTree": "ツリーナビゲーションでファイルを非表示",
|
||||
"hideFilesInTreeDescription": "有効にすると、サイドバーのツリーナビゲーションにフォルダーのみが表示されます",
|
||||
"previewModelsDescription": "3Dモデルのサムネイルプレビューを表示します。無効の場合は一般的なアイコンを表示します",
|
||||
"previewAudiosDescription": "オーディオのサムネイルプレビューを表示します。無効の場合は一般的なアイコンを表示します",
|
||||
"setDateFormat": "正確な日付書式を設定する",
|
||||
"showHiddenFiles": "隠しファイルを表示する",
|
||||
"showHiddenFilesDescription": "隠しファイル(.thumbnails、ProgramDataなど)をリストに表示します。インデックスが設定されている場合は手動でアクセスできます。",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "저장 및 종료",
|
||||
"keepEditing": "계속 편집",
|
||||
"closeAnyway": "어쨌든 닫기",
|
||||
"goToItem": "항목으로 이동",
|
||||
"watchFile": "파일 보기",
|
||||
"showMore": "고급 옵션 표시",
|
||||
"showLess": "고급 옵션 숨기기"
|
||||
"showLess": "고급 옵션 숨기기",
|
||||
"goToSource": "소스 위치로 이동",
|
||||
"goToItem": "항목으로 이동"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "파일 다운로드",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "대상 폴더를 선택한 다음 아래에 아카이브 이름을 입력합니다:",
|
||||
"archiveName": "아카이브 이름(기본값은 archive.zip):",
|
||||
"archiveCompression": "압축 수준(0-9):",
|
||||
"archiveDeleteAfter": "생성 성공 후 소스 파일 삭제",
|
||||
"archiveSuccess": "아카이브가 성공적으로 생성되었습니다!",
|
||||
"unarchive": "아카이브 추출",
|
||||
"unarchiveMessage": "아카이브를 추출할 위치를 선택합니다.",
|
||||
"unarchiveDestination": "추출 대상:",
|
||||
"unarchiveDeleteAfter": "추출 후 아카이브 삭제",
|
||||
"unarchiveSuccess": "아카이브가 성공적으로 추출되었습니다!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "OS에서 보고한 디스크/파티션 크기에서 사용량 막대를 표시합니다. 이 크기는 더 클 수 있으며 인덱싱되지 않은 파일도 포함됩니다.",
|
||||
"usageTextDisplay": "어떤 사용법 텍스트를 표시해야 하나요?",
|
||||
"usageTextIndexed": "인덱싱된 사용량",
|
||||
"usageTextDisk": "디스크/파티션별로 보고된 사용량"
|
||||
"usageTextDisk": "디스크/파티션별로 보고된 사용량",
|
||||
"dividerNamePlaceholder": "디바이더 텍스트(줄을 표시하려면 비워둡니다)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "링크가 복사되었습니다!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "결과{suffix}",
|
||||
"users": "사용자{suffix}",
|
||||
"photos": "사진",
|
||||
"audio": "오디오{suffix}",
|
||||
"audios": "오디오{suffix}",
|
||||
"videos": "동영상{suffix}",
|
||||
"models": "3D 모델",
|
||||
"documents": "문서{suffix}",
|
||||
"archives": "아카이브{suffix}",
|
||||
"images": "이미지{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "공간{suffix}",
|
||||
"format": "형식{suffix}",
|
||||
"office": "Office{suffix}",
|
||||
"show": "표시{suffix}"
|
||||
"show": "표시{suffix}",
|
||||
"divider": "디바이더{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "액세스 규칙",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "오렌지"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "아카이브 생성/추출 후 소스 파일 삭제",
|
||||
"deleteAfterArchiveDescription": "활성화되면 아카이브 생성/추출 성공 후 소스 파일을 삭제합니다",
|
||||
"hideFilesInTree": "트리 탐색에서 파일 숨기기",
|
||||
"hideFilesInTreeDescription": "활성화되면 사이드바 트리 탐색에 폴더만 표시됩니다",
|
||||
"previewModelsDescription": "3D 모델의 썸네일 미리보기를 표시합니다. 비활성화된 경우 일반 아이콘이 표시됩니다",
|
||||
"previewAudiosDescription": "오디오의 썸네일 미리보기를 표시합니다. 비활성화된 경우 일반 아이콘이 표시됩니다",
|
||||
"setDateFormat": "정확한 날짜 형식 설정",
|
||||
"showHiddenFiles": "숨겨진 파일 표시",
|
||||
"showHiddenFilesDescription": "목록에 숨겨진 파일(예: .썸네일, 프로그램 데이터)을 표시하지만 색인화하도록 구성한 경우 여전히 수동으로 액세스할 수 있습니다.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Opslaan en afsluiten",
|
||||
"keepEditing": "Blijven bewerken",
|
||||
"closeAnyway": "Sluiten",
|
||||
"goToItem": "Ga naar item",
|
||||
"watchFile": "Bestand bekijken",
|
||||
"showMore": "Geavanceerde opties weergeven",
|
||||
"showLess": "Geavanceerde opties verbergen"
|
||||
"showLess": "Geavanceerde opties verbergen",
|
||||
"goToSource": "Ga naar de bronlocatie",
|
||||
"goToItem": "Ga naar item"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Bestand downloaden",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Kies een bestemmingsmap (voer hieronder de naam van het archief in):",
|
||||
"archiveName": "Archiefnaam (standaard archive.zip):",
|
||||
"archiveCompression": "Compressieniveau (0-9):",
|
||||
"archiveDeleteAfter": "Bronbestanden verwijderen na succesvolle creatie",
|
||||
"archiveSuccess": "Archief succesvol aangemaakt!",
|
||||
"unarchive": "Archief uitpakken",
|
||||
"unarchiveMessage": "Kies waar het archief moet worden uitgepakt.",
|
||||
"unarchiveDestination": "Uittreksel naar:",
|
||||
"unarchiveDeleteAfter": "Archief verwijderen na uitpakken",
|
||||
"unarchiveSuccess": "Archief met succes uitgepakt!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Geef de verbruiksbalk weer van de grootte van de schijf/partitie zoals gerapporteerd door het besturingssysteem. Deze grootte zal waarschijnlijk groter zijn en omvat bestanden die niet geïndexeerd zijn.",
|
||||
"usageTextDisplay": "Welke gebruikstekst moet worden weergegeven?",
|
||||
"usageTextIndexed": "gebruik zoals geïndexeerd",
|
||||
"usageTextDisk": "gebruik zoals gerapporteerd door schijf/partitie"
|
||||
"usageTextDisk": "gebruik zoals gerapporteerd door schijf/partitie",
|
||||
"dividerNamePlaceholder": "Scheidingstekst (leeg laten om een lijn weer te geven)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link gekopieerd!"
|
||||
@@ -550,6 +550,12 @@
|
||||
"orange": "oranje"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Bronbestanden verwijderen na aanmaken/uitpakken van archieven",
|
||||
"deleteAfterArchiveDescription": "Indien ingeschakeld, worden de bronbestanden verwijderd na succesvol aanmaken/uitpakken van archieven",
|
||||
"hideFilesInTree": "Bestanden verbergen in boomnavigatie",
|
||||
"hideFilesInTreeDescription": "Indien ingeschakeld, worden alleen mappen weergegeven in de zijbalk Boomnavigatie",
|
||||
"previewModelsDescription": "Toon miniatuurweergaven voor 3D-modellen, er wordt een algemeen pictogram getoond indien uitgeschakeld",
|
||||
"previewAudiosDescription": "Toon miniatuurweergaven voor audiobestanden, er wordt een algemeen pictogram getoond indien uitgeschakeld",
|
||||
"setDateFormat": "Exact datumformaat instellen",
|
||||
"showHiddenFiles": "Verborgen bestanden weergeven",
|
||||
"showHiddenFilesDescription": "Toon verborgen bestanden in de lijst (bijv. .thumbnails, ProgramData), maar ze zijn nog steeds handmatig toegankelijk als ze zijn geconfigureerd om te worden geïndexeerd.",
|
||||
@@ -642,7 +648,8 @@
|
||||
"results": "Resultaten{suffix}",
|
||||
"users": "Gebruikers{suffix}",
|
||||
"photos": "Foto's",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Audio's{suffix}",
|
||||
"models": "3D-modellen",
|
||||
"videos": "Video's{suffix}",
|
||||
"documents": "Documenten{suffix}",
|
||||
"archives": "Archief{suffix}",
|
||||
@@ -726,7 +733,8 @@
|
||||
"space": "Ruimte{suffix}",
|
||||
"format": "Formaat{suffix}",
|
||||
"office": "Kantoor{suffix}",
|
||||
"show": "Toon{suffix}"
|
||||
"show": "Toon{suffix}",
|
||||
"divider": "Verdeler{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Toegangsregels",
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
"scopes": "Bereiken{suffix}",
|
||||
"commands": "Opdrachten{suffix}",
|
||||
"photos": "Foto's",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Audio{suffix}",
|
||||
"models": "3D-modellen",
|
||||
"videos": "Video's{suffix}",
|
||||
"documents": "Documenten{suffix}",
|
||||
"archives": "Archieven{suffix}",
|
||||
@@ -126,7 +127,8 @@
|
||||
"space": "Ruimte{suffix}",
|
||||
"format": "Formaat{suffix}",
|
||||
"office": "Kantoor{suffix}",
|
||||
"show": "Toon{suffix}"
|
||||
"show": "Toon{suffix}",
|
||||
"divider": "Verdeler{suffix}"
|
||||
},
|
||||
"buttons": {
|
||||
"generateNewOtp": "Herstellen en nieuwe twee-factorcode genereren",
|
||||
@@ -160,6 +162,7 @@
|
||||
"saveAndExit": "Opslaan en afsluiten",
|
||||
"keepEditing": "Blijven bewerken",
|
||||
"closeAnyway": "Sluiten",
|
||||
"goToSource": "Ga naar de bronlocatie",
|
||||
"goToItem": "Ga naar item"
|
||||
},
|
||||
"download": {
|
||||
@@ -428,12 +431,10 @@
|
||||
"archiveDestination": "Kies een bestemmingsmap (voer hieronder de naam van het archief in):",
|
||||
"archiveName": "Archiefnaam (standaard archive.zip):",
|
||||
"archiveCompression": "Compressieniveau (0-9):",
|
||||
"archiveDeleteAfter": "Bronbestanden verwijderen na succesvolle creatie",
|
||||
"archiveSuccess": "Archief succesvol aangemaakt!",
|
||||
"unarchive": "Archief uitpakken",
|
||||
"unarchiveMessage": "Kies waar het archief moet worden uitgepakt.",
|
||||
"unarchiveDestination": "Uittreksel naar:",
|
||||
"unarchiveDeleteAfter": "Archief verwijderen na uitpakken",
|
||||
"unarchiveSuccess": "Archief met succes uitgepakt!"
|
||||
},
|
||||
"search": {
|
||||
@@ -452,6 +453,12 @@
|
||||
"helpText2": "De index: Zoeken maakt gebruik van de index die automatisch wordt bijgewerkt op het ingestelde interval (standaard: 5 minuten). Zoeken als het programma net is gestart, kan onvolledige resultaten opleveren."
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Bronbestanden verwijderen na aanmaken/uitpakken van archieven",
|
||||
"deleteAfterArchiveDescription": "Indien ingeschakeld, worden de bronbestanden verwijderd na succesvol aanmaken/uitpakken van archieven",
|
||||
"hideFilesInTree": "Bestanden verbergen in boomnavigatie",
|
||||
"hideFilesInTreeDescription": "Indien ingeschakeld, worden alleen mappen weergegeven in de zijbalk Boomnavigatie",
|
||||
"previewModelsDescription": "Toon miniatuurweergaven voor 3D-modellen, er wordt een algemeen pictogram getoond indien uitgeschakeld",
|
||||
"previewAudiosDescription": "Toon miniatuurweergaven voor audiobestanden, er wordt een algemeen pictogram getoond indien uitgeschakeld",
|
||||
"debugOfficeEditor": "OnlyOffice foutopsporingsmodus inschakelen",
|
||||
"debugOfficeEditorDescription": "Een foutopsporingspop-up weergeven met aanvullende informatie in de OnlyOffice-editor.",
|
||||
"defaultMediaPlayer": "De ingebouwde mediaspeler van uw browser gebruiken",
|
||||
@@ -646,7 +653,8 @@
|
||||
"showDiskUsageDescription": "Geef de verbruiksbalk weer van de schijf-/partitiegrootte zoals gerapporteerd door het besturingssysteem. Deze grootte zal waarschijnlijk groter zijn en omvat bestanden die niet geïndexeerd zijn.",
|
||||
"usageTextDisplay": "Welke gebruikstekst moet worden weergegeven?",
|
||||
"usageTextIndexed": "gebruik zoals geïndexeerd",
|
||||
"usageTextDisk": "gebruik zoals gerapporteerd door schijf/partitie"
|
||||
"usageTextDisk": "gebruik zoals gerapporteerd door schijf/partitie",
|
||||
"dividerNamePlaceholder": "Scheidingstekst (leeg laten om een lijn weer te geven)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Koppeling gekopieerd!"
|
||||
@@ -915,4 +923,4 @@
|
||||
"editorSuccess": "Initialisatie Editor - Succes! Alle stappen voltooid.",
|
||||
"setupError": "Setup-fout"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Zapisz i wyjdź",
|
||||
"keepEditing": "Kontynuuj edycję",
|
||||
"closeAnyway": "W każdym razie blisko",
|
||||
"goToItem": "Przejdź do pozycji",
|
||||
"watchFile": "Oglądaj plik",
|
||||
"showMore": "Pokaż opcje zaawansowane",
|
||||
"showLess": "Ukryj opcje zaawansowane"
|
||||
"showLess": "Ukryj opcje zaawansowane",
|
||||
"goToSource": "Przejdź do lokalizacji źródłowej",
|
||||
"goToItem": "Przejdź do pozycji"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Pobierz plik",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Wybierz folder docelowy (a następnie wprowadź nazwę archiwum poniżej):",
|
||||
"archiveName": "Nazwa archiwum (domyślnie archive.zip):",
|
||||
"archiveCompression": "Poziom kompresji (0-9):",
|
||||
"archiveDeleteAfter": "Usuwanie plików źródłowych po pomyślnym utworzeniu",
|
||||
"archiveSuccess": "Archiwum utworzone pomyślnie!",
|
||||
"unarchive": "Wyodrębnij archiwum",
|
||||
"unarchiveMessage": "Wybierz miejsce rozpakowania archiwum.",
|
||||
"unarchiveDestination": "Wyciąg do:",
|
||||
"unarchiveDeleteAfter": "Usuń archiwum po wyodrębnieniu",
|
||||
"unarchiveSuccess": "Archiwum rozpakowane pomyślnie!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Wyświetla pasek użycia na podstawie rozmiaru dysku/partycji zgłoszonego przez system operacyjny. Rozmiar ten będzie prawdopodobnie większy i obejmuje pliki niezindeksowane.",
|
||||
"usageTextDisplay": "Jaki tekst użytkowania powinien być wyświetlany?",
|
||||
"usageTextIndexed": "użycie jako indeksowane",
|
||||
"usageTextDisk": "użycie zgłoszone przez dysk/partycję"
|
||||
"usageTextDisk": "użycie zgłoszone przez dysk/partycję",
|
||||
"dividerNamePlaceholder": "Tekst podziału (pozostaw pusty, aby wyświetlić linię)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link Skopiowany!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Wyniki{suffix}",
|
||||
"users": "Użytkownicy{suffix}",
|
||||
"photos": "Zdjęcia",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Audios{suffix}",
|
||||
"videos": "Filmy{suffix}",
|
||||
"models": "Modele 3D",
|
||||
"documents": "Dokumenty{suffix}",
|
||||
"archives": "Archiwa{suffix}",
|
||||
"images": "Obrazy{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Przestrzeń{suffix}",
|
||||
"format": "Format{suffix}",
|
||||
"office": "Biuro{suffix}",
|
||||
"show": "Pokaż{suffix}"
|
||||
"show": "Pokaż{suffix}",
|
||||
"divider": "Dzielnik{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Zasady dostępu",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "pomarańczowy"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Usuń pliki źródłowe po utworzeniu/wypakowaniu archiwów",
|
||||
"deleteAfterArchiveDescription": "Po włączeniu, usunie pliki źródłowe po pomyślnym utworzeniu/wypakowaniu archiwów",
|
||||
"hideFilesInTree": "Ukryj pliki w nawigacji drzewiastej",
|
||||
"hideFilesInTreeDescription": "Po włączeniu, w drzewiastej nawigacji na pasku bocznym wyświetlane są tylko foldery",
|
||||
"previewModelsDescription": "Pokaż miniatury podglądów dla modeli 3D, jeśli wyłączone, pojawi się ogólna ikona",
|
||||
"previewAudiosDescription": "Pokaż miniatury podglądów dla plików audio, jeśli wyłączone, pojawi się ogólna ikona",
|
||||
"setDateFormat": "Ustaw dokładny format daty",
|
||||
"showHiddenFiles": "Pokaż ukryte pliki",
|
||||
"showHiddenFilesDescription": "Pokaż ukryte pliki na liście (np. .thumbnails, ProgramData), ale są one nadal dostępne ręcznie, jeśli są skonfigurowane do indeksowania.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Salvar e sair",
|
||||
"keepEditing": "Continuar editando",
|
||||
"closeAnyway": "Fechar de qualquer maneira",
|
||||
"goToItem": "Ir para o item",
|
||||
"watchFile": "Assista ao arquivo",
|
||||
"showMore": "Mostrar opções avançadas",
|
||||
"showLess": "Ocultar opções avançadas"
|
||||
"showLess": "Ocultar opções avançadas",
|
||||
"goToSource": "Ir para o local de origem",
|
||||
"goToItem": "Ir para o item"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Baixar arquivo",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Escolha a pasta de destino (depois insira o nome do arquivo abaixo):",
|
||||
"archiveName": "Nome do arquivo (padrão archive.zip):",
|
||||
"archiveCompression": "Nível de compressão (0-9):",
|
||||
"archiveDeleteAfter": "Excluir arquivos de origem após a criação bem-sucedida",
|
||||
"archiveSuccess": "Arquivo criado com sucesso!",
|
||||
"unarchive": "Extrair arquivo",
|
||||
"unarchiveMessage": "Escolha onde extrair o arquivo.",
|
||||
"unarchiveDestination": "Extrair para:",
|
||||
"unarchiveDeleteAfter": "Excluir o arquivo após a extração",
|
||||
"unarchiveSuccess": "Arquivo extraído com sucesso!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Exibe a barra de uso do tamanho do disco/partição conforme relatado pelo sistema operacional. Esse tamanho provavelmente será maior e inclui arquivos não indexados.",
|
||||
"usageTextDisplay": "Qual texto de uso deve ser exibido?",
|
||||
"usageTextIndexed": "uso conforme indexado",
|
||||
"usageTextDisk": "uso conforme relatado pelo disco/partição"
|
||||
"usageTextDisk": "uso conforme relatado pelo disco/partição",
|
||||
"dividerNamePlaceholder": "Texto do divisor (deixe em branco para mostrar uma linha)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link copiado!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Resultados{suffix}",
|
||||
"users": "Usuários{suffix}",
|
||||
"photos": "Fotos",
|
||||
"audio": "Áudio{suffix}",
|
||||
"audios": "Áudios{suffix}",
|
||||
"videos": "Vídeos{suffix}",
|
||||
"models": "Modelos 3D",
|
||||
"documents": "Documentos{suffix}",
|
||||
"archives": "Arquivos{suffix}",
|
||||
"images": "Imagens{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Espaço{suffix}",
|
||||
"format": "Formato{suffix}",
|
||||
"office": "Escritório{suffix}",
|
||||
"show": "Mostrar{suffix}"
|
||||
"show": "Mostrar{suffix}",
|
||||
"divider": "Divisor{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regras de acesso",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "laranja"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Excluir arquivos de origem após criação/extração de arquivos",
|
||||
"deleteAfterArchiveDescription": "Quando ativado, excluirá os arquivos de origem após criação/extração bem-sucedida de arquivos",
|
||||
"hideFilesInTree": "Ocultar arquivos na navegação em árvore",
|
||||
"hideFilesInTreeDescription": "Quando ativado, apenas pastas são mostradas na navegação em árvore da barra lateral",
|
||||
"previewModelsDescription": "Mostrar visualizações em miniatura para modelos 3D, um ícone genérico será mostrado se desativado",
|
||||
"previewAudiosDescription": "Mostrar pré-visualizações em miniatura para áudios, será mostrado um ícone genérico se desativado",
|
||||
"setDateFormat": "Definir o formato exato da data",
|
||||
"showHiddenFiles": "Mostrar arquivos ocultos",
|
||||
"showHiddenFilesDescription": "Mostrar arquivos ocultos na listagem (por exemplo, .thumbnails, ProgramData), mas eles ainda poderão ser acessados manualmente se estiverem configurados para serem indexados.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Guardar e sair",
|
||||
"keepEditing": "Continuar a editar",
|
||||
"closeAnyway": "Fechar de qualquer maneira",
|
||||
"goToItem": "Ir para o item",
|
||||
"watchFile": "Ver ficheiro",
|
||||
"showMore": "Mostrar opções avançadas",
|
||||
"showLess": "Ocultar opções avançadas"
|
||||
"showLess": "Ocultar opções avançadas",
|
||||
"goToSource": "Ir para o local de origem",
|
||||
"goToItem": "Ir para o item"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarregar ficheiro",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Escolha a pasta de destino (depois introduza o nome do arquivo abaixo):",
|
||||
"archiveName": "Nome do ficheiro (por defeito archive.zip):",
|
||||
"archiveCompression": "Nível de compressão (0-9):",
|
||||
"archiveDeleteAfter": "Eliminar ficheiros de origem após a criação bem sucedida",
|
||||
"archiveSuccess": "Arquivo criado com sucesso!",
|
||||
"unarchive": "Extrair arquivo",
|
||||
"unarchiveMessage": "Escolha onde extrair o arquivo.",
|
||||
"unarchiveDestination": "Extrair para:",
|
||||
"unarchiveDeleteAfter": "Eliminar o arquivo após a extração",
|
||||
"unarchiveSuccess": "Arquivo extraído com sucesso!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Apresenta a barra de utilização a partir do tamanho do disco/partição, conforme indicado pelo SO. Este tamanho será provavelmente maior e inclui ficheiros não indexados.",
|
||||
"usageTextDisplay": "Que texto de utilização deve ser apresentado?",
|
||||
"usageTextIndexed": "utilização como indexado",
|
||||
"usageTextDisk": "utilização conforme registada por disco/partição"
|
||||
"usageTextDisk": "utilização conforme registada por disco/partição",
|
||||
"dividerNamePlaceholder": "Texto do separador (deixar em branco para mostrar uma linha)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link copiado!"
|
||||
@@ -550,6 +550,12 @@
|
||||
"orange": "laranja"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Eliminar ficheiros de origem após criação/extração de arquivos",
|
||||
"deleteAfterArchiveDescription": "Quando ativado, eliminará os ficheiros de origem após criação/extração bem-sucedida de arquivos",
|
||||
"hideFilesInTree": "Ocultar ficheiros na navegação em árvore",
|
||||
"hideFilesInTreeDescription": "Quando ativado, apenas pastas são mostradas na navegação em árvore da barra lateral",
|
||||
"previewModelsDescription": "Mostrar pré-visualizações em miniatura para modelos 3D, será mostrado um ícone genérico se desativado",
|
||||
"previewAudiosDescription": "Mostrar visualizações em miniatura para áudios, um ícone genérico será mostrado se desativado",
|
||||
"setDateFormat": "Definir o formato exato da data",
|
||||
"showHiddenFiles": "Mostrar ficheiros ocultos",
|
||||
"showHiddenFilesDescription": "Mostrar ficheiros ocultos na listagem (por exemplo, .thumbnails, ProgramData), mas estes continuam a ser acessíveis manualmente se estiverem configurados para serem indexados.",
|
||||
@@ -642,8 +648,9 @@
|
||||
"results": "Resultados{suffix}",
|
||||
"users": "Utilizadores{suffix}",
|
||||
"photos": "Fotos",
|
||||
"audio": "Áudio{suffix}",
|
||||
"audios": "Áudios{suffix}",
|
||||
"videos": "Vídeos{suffix}",
|
||||
"models": "Modelos 3D",
|
||||
"documents": "Documentos{suffix}",
|
||||
"archives": "Arquivos{suffix}",
|
||||
"images": "Imagens{suffix}",
|
||||
@@ -726,7 +733,8 @@
|
||||
"space": "Espaço{suffix}",
|
||||
"format": "Formato{suffix}",
|
||||
"office": "Escritório{suffix}",
|
||||
"show": "Mostrar{suffix}"
|
||||
"show": "Mostrar{suffix}",
|
||||
"divider": "Divisor{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regras de acesso",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Salvare și ieșire",
|
||||
"keepEditing": "Continuă editarea",
|
||||
"closeAnyway": "Închide oricum",
|
||||
"goToItem": "Du-te la articol",
|
||||
"watchFile": "Fișier de vizionare",
|
||||
"showMore": "Afișare opțiuni avansate",
|
||||
"showLess": "Ascundeți opțiunile avansate"
|
||||
"showLess": "Ascundeți opțiunile avansate",
|
||||
"goToSource": "Du-te la locația sursei",
|
||||
"goToItem": "Du-te la articol"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarcă fișier",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Alegeți folderul de destinație (apoi introduceți numele arhivei mai jos):",
|
||||
"archiveName": "Numele arhivei (implicit archive.zip):",
|
||||
"archiveCompression": "Nivelul de compresie (0-9):",
|
||||
"archiveDeleteAfter": "Ștergeți fișierele sursă după crearea cu succes",
|
||||
"archiveSuccess": "Arhiva creată cu succes!",
|
||||
"unarchive": "Extras arhivă",
|
||||
"unarchiveMessage": "Alegeți unde să extrageți arhiva.",
|
||||
"unarchiveDestination": "Extras la:",
|
||||
"unarchiveDeleteAfter": "Ștergeți arhiva după extragere",
|
||||
"unarchiveSuccess": "Arhiva extrasă cu succes!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Afișează bara de utilizare din dimensiunea discului/partiției raportată de sistemul de operare. Această dimensiune va fi probabil mai mare și include fișiere care nu sunt indexate.",
|
||||
"usageTextDisplay": "Ce text de utilizare ar trebui să fie afișat?",
|
||||
"usageTextIndexed": "utilizare ca indexat",
|
||||
"usageTextDisk": "utilizare raportată de disc/partiție"
|
||||
"usageTextDisk": "utilizare raportată de disc/partiție",
|
||||
"dividerNamePlaceholder": "Textul separatorului (lăsați gol pentru a afișa o linie)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Legătură copiată!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Rezultate{suffix}",
|
||||
"users": "Utilizatori{suffix}",
|
||||
"photos": "Fotografii",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Audios{suffix}",
|
||||
"videos": "Video{suffix}",
|
||||
"models": "Modele 3D",
|
||||
"documents": "Documente{suffix}",
|
||||
"archives": "Arhive{suffix}",
|
||||
"images": "Imagini{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Spațiu{suffix}",
|
||||
"format": "Format{suffix}",
|
||||
"office": "Birou{suffix}",
|
||||
"show": "Spectacol{suffix}"
|
||||
"show": "Spectacol{suffix}",
|
||||
"divider": "Divizor{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Reguli de acces",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "portocaliu"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Șterge fișierele sursă după crearea/extragerea arhivelor",
|
||||
"deleteAfterArchiveDescription": "Când este activat, va șterge fișierele sursă după crearea/extragerea cu succes a arhivelor",
|
||||
"hideFilesInTree": "Ascunde fișierele în navigarea arborescentă",
|
||||
"hideFilesInTreeDescription": "Când este activat, numai folderele sunt afișate în navigarea arborescentă a barei laterale",
|
||||
"previewModelsDescription": "Afișează previzualizări miniatură pentru modele 3D, se va afișa o pictogramă generică dacă este dezactivat",
|
||||
"previewAudiosDescription": "Afișează previzualizări miniatură pentru fișiere audio, se va afișa o pictogramă generică dacă este dezactivat",
|
||||
"setDateFormat": "Setați formatul exact al datei",
|
||||
"showHiddenFiles": "Afișează fișierele ascunse",
|
||||
"showHiddenFilesDescription": "Afișează fișierele ascunse în listă (de exemplu, .thumbnails, ProgramData), dar acestea sunt încă accesibile manual dacă sunt configurate pentru a fi indexate.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Сохранить и выйти",
|
||||
"keepEditing": "Продолжить редактирование",
|
||||
"closeAnyway": "Закрыть в любом случае",
|
||||
"goToItem": "Перейти к пункту",
|
||||
"watchFile": "Смотреть файл",
|
||||
"showMore": "Показать дополнительные параметры",
|
||||
"showLess": "Скрыть дополнительные параметры"
|
||||
"showLess": "Скрыть дополнительные параметры",
|
||||
"goToSource": "Перейти к исходному местоположению",
|
||||
"goToItem": "Перейти к пункту"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Скачать файл",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Выберите папку назначения (затем введите имя архива ниже):",
|
||||
"archiveName": "Имя архива (по умолчанию archive.zip):",
|
||||
"archiveCompression": "Уровень сжатия (0-9):",
|
||||
"archiveDeleteAfter": "Удаление исходных файлов после успешного создания",
|
||||
"archiveSuccess": "Архив создан успешно!",
|
||||
"unarchive": "Распаковать архив",
|
||||
"unarchiveMessage": "Выберите, куда распаковать архив.",
|
||||
"unarchiveDestination": "Отрывок в:",
|
||||
"unarchiveDeleteAfter": "Удаление архива после извлечения",
|
||||
"unarchiveSuccess": "Архив извлечен успешно!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Отображение полосы использования от размера диска/раздела, о котором сообщает ОС. Этот размер может быть больше и включать неиндексированные файлы.",
|
||||
"usageTextDisplay": "Какой текст должен отображаться?",
|
||||
"usageTextIndexed": "использование в индексированном виде",
|
||||
"usageTextDisk": "использование по данным диска/раздела"
|
||||
"usageTextDisk": "использование по данным диска/раздела",
|
||||
"dividerNamePlaceholder": "Текст разделителя (оставьте пустым, чтобы показать строку)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Ссылка скопирована!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Результаты{suffix}",
|
||||
"users": "Пользователи{suffix}",
|
||||
"photos": "Фотографии",
|
||||
"audio": "Аудио{suffix}",
|
||||
"audios": "Аудио{suffix}",
|
||||
"videos": "Видео{suffix}",
|
||||
"models": "3D-модели",
|
||||
"documents": "Документы{suffix}",
|
||||
"archives": "Архивы{suffix}",
|
||||
"images": "Изображения{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Космос{suffix}",
|
||||
"format": "Формат{suffix}",
|
||||
"office": "Офис{suffix}",
|
||||
"show": "Показать{suffix}"
|
||||
"show": "Показать{suffix}",
|
||||
"divider": "Разделитель{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Правила доступа",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "оранжевый"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Удалять исходные файлы после создания/извлечения архивов",
|
||||
"deleteAfterArchiveDescription": "При включении удалит исходные файлы после успешного создания/извлечения архивов",
|
||||
"hideFilesInTree": "Скрывать файлы в древовидной навигации",
|
||||
"hideFilesInTreeDescription": "При включении в древовидной навигации боковой панели отображаются только папки",
|
||||
"previewModelsDescription": "Показывать миниатюры для 3D-моделей, если отключено, будет показана стандартная иконка",
|
||||
"previewAudiosDescription": "Показывать миниатюры для аудиофайлов, если отключено, будет показана стандартная иконка",
|
||||
"setDateFormat": "Установить точный формат даты",
|
||||
"showHiddenFiles": "Показывать скрытые файлы",
|
||||
"showHiddenFilesDescription": "Показывать в списке скрытые файлы (например, .thumbnails, ProgramData), но они по-прежнему доступны вручную, если настроены на индексирование.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Uložiť a ukončiť",
|
||||
"keepEditing": "Pokračujte v úpravách",
|
||||
"closeAnyway": "Zavrieť Tak či onak",
|
||||
"goToItem": "Prejsť na položku",
|
||||
"watchFile": "Sledovať súbor",
|
||||
"showMore": "Zobraziť rozšírené možnosti",
|
||||
"showLess": "Skryť rozšírené možnosti"
|
||||
"showLess": "Skryť rozšírené možnosti",
|
||||
"goToSource": "Prejsť na zdrojové miesto",
|
||||
"goToItem": "Prejsť na položku"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stiahnuť súbor",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Vyberte cieľový priečinok (potom zadajte názov archívu nižšie):",
|
||||
"archiveName": "Názov archívu (štandardne archive.zip):",
|
||||
"archiveCompression": "Úroveň kompresie (0-9):",
|
||||
"archiveDeleteAfter": "Odstránenie zdrojových súborov po úspešnom vytvorení",
|
||||
"archiveSuccess": "Archív bol úspešne vytvorený!",
|
||||
"unarchive": "Výpis archívu",
|
||||
"unarchiveMessage": "Vyberte, kam sa má archív extrahovať.",
|
||||
"unarchiveDestination": "Výpis na:",
|
||||
"unarchiveDeleteAfter": "Odstránenie archívu po extrakcii",
|
||||
"unarchiveSuccess": "Archív úspešne extrahovaný!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Zobrazenie lišty využitia z veľkosti disku/oddielu podľa hlásenia operačného systému. Táto veľkosť bude pravdepodobne väčšia a zahŕňa aj súbory, ktoré nie sú indexované.",
|
||||
"usageTextDisplay": "Ktorý text použitia sa má zobraziť?",
|
||||
"usageTextIndexed": "použitie ako indexované",
|
||||
"usageTextDisk": "využitie podľa hlásenia disku/oddielu"
|
||||
"usageTextDisk": "využitie podľa hlásenia disku/oddielu",
|
||||
"dividerNamePlaceholder": "Text oddeľovača (nechajte prázdny, aby sa zobrazil riadok)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Odkaz skopírovaný!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Výsledky{suffix}",
|
||||
"users": "Používatelia{suffix}",
|
||||
"photos": "Fotografie",
|
||||
"audio": "Audio{suffix}",
|
||||
"audios": "Zvuky{suffix}",
|
||||
"videos": "Videá{suffix}",
|
||||
"models": "3D modely",
|
||||
"documents": "Dokumenty{suffix}",
|
||||
"archives": "Archív{suffix}",
|
||||
"images": "Obrázky{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Priestor{suffix}",
|
||||
"format": "Formát{suffix}",
|
||||
"office": "Kancelária{suffix}",
|
||||
"show": "Zobraziť{suffix}"
|
||||
"show": "Zobraziť{suffix}",
|
||||
"divider": "Rozdeľovač{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Pravidlá prístupu",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "oranžová"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Odstrániť zdrojové súbory po vytvorení/extrahovaní archívov",
|
||||
"deleteAfterArchiveDescription": "Ak je povolené, po úspešnom vytvorení/extrahovaní archívov odstráni zdrojové súbory",
|
||||
"hideFilesInTree": "Skryť súbory v stromovej navigácii",
|
||||
"hideFilesInTreeDescription": "Ak je povolené, v stromovej navigácii bočného panela sa zobrazujú iba priečinky",
|
||||
"previewModelsDescription": "Zobraziť miniatúry náhľadov pre 3D modely, ak je zakázané, zobrazí sa všeobecná ikona",
|
||||
"previewAudiosDescription": "Zobraziť miniatúry náhľadov pre zvuky, ak je zakázané, zobrazí sa všeobecná ikona",
|
||||
"setDateFormat": "Nastavenie presného formátu dátumu",
|
||||
"showHiddenFiles": "Zobrazenie skrytých súborov",
|
||||
"showHiddenFilesDescription": "Zobrazenie skrytých súborov vo výpise (napr. .thumbnails, ProgramData), ale sú stále manuálne prístupné, ak sú nakonfigurované na indexovanie.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Spara och avsluta",
|
||||
"keepEditing": "Fortsätt redigera",
|
||||
"closeAnyway": "Stäng i alla fall",
|
||||
"goToItem": "Gå till artikel",
|
||||
"watchFile": "Titta på filen",
|
||||
"showMore": "Visa avancerade alternativ",
|
||||
"showLess": "Dölj avancerade alternativ"
|
||||
"showLess": "Dölj avancerade alternativ",
|
||||
"goToSource": "Gå till källplatsen",
|
||||
"goToItem": "Gå till objektet"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Ladda ner fil",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Välj destinationsmapp (ange sedan arkivnamn nedan):",
|
||||
"archiveName": "Arkivnamn (standard archive.zip):",
|
||||
"archiveCompression": "Kompressionsnivå (0-9):",
|
||||
"archiveDeleteAfter": "Ta bort källfiler efter lyckad skapelse",
|
||||
"archiveSuccess": "Arkiv skapat framgångsrikt!",
|
||||
"unarchive": "Extrahera arkiv",
|
||||
"unarchiveMessage": "Välj var du vill extrahera arkivet.",
|
||||
"unarchiveDestination": "Extrahera till:",
|
||||
"unarchiveDeleteAfter": "Ta bort arkiv efter extraktion",
|
||||
"unarchiveSuccess": "Arkivet har extraherats framgångsrikt!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Visa användningsstapel från disk-/partitionsstorlek som rapporteras av operativsystemet. Denna storlek är sannolikt större och inkluderar filer som inte är indexerade.",
|
||||
"usageTextDisplay": "Vilken användningstext ska visas?",
|
||||
"usageTextIndexed": "användning som indexerad",
|
||||
"usageTextDisk": "användning som rapporteras av disk/partition"
|
||||
"usageTextDisk": "användning som rapporteras av disk/partition",
|
||||
"dividerNamePlaceholder": "Delningstext (lämna tom för att visa en rad)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Länk kopierad"
|
||||
@@ -550,6 +550,12 @@
|
||||
"orange": "apelsin"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Ta bort källfiler efter skapande/extrahering av arkiv",
|
||||
"deleteAfterArchiveDescription": "När aktiverad kommer att ta bort källfilerna efter framgångsrik skapande/extrahering av arkiv",
|
||||
"hideFilesInTree": "Dölj filer i trädnavigering",
|
||||
"hideFilesInTreeDescription": "När aktiverad visas endast mappar i sidofältets trädnavigering",
|
||||
"previewModelsDescription": "Visa miniatyrförhandsvisningar för 3D-modeller, kommer att visa generisk ikon om inaktiverad",
|
||||
"previewAudiosDescription": "Visa miniatyrförhandsvisningar för ljudfiler, kommer att visa generisk ikon om inaktiverad",
|
||||
"setDateFormat": "Ange exakt datumformat",
|
||||
"showHiddenFiles": "Visa dolda filer",
|
||||
"showHiddenFilesDescription": "Visa dolda filer i listan (t.ex. .thumbnails, ProgramData), men de är fortfarande manuellt åtkomliga om de är konfigurerade för att indexeras.",
|
||||
@@ -642,8 +648,9 @@
|
||||
"results": "Resultat{suffix}",
|
||||
"users": "Användare{suffix}",
|
||||
"photos": "Bilder",
|
||||
"audio": "Ljud{suffix}",
|
||||
"audios": "Ljud{suffix}",
|
||||
"videos": "Videor{suffix}",
|
||||
"models": "3D-modeller",
|
||||
"documents": "Dokument{suffix}",
|
||||
"archives": "Arkiv{suffix}",
|
||||
"images": "Bilder{suffix}",
|
||||
@@ -726,7 +733,8 @@
|
||||
"space": "Utrymme{suffix}",
|
||||
"format": "Format{suffix}",
|
||||
"office": "Kontor{suffix}",
|
||||
"show": "Visa{suffix}"
|
||||
"show": "Visa{suffix}",
|
||||
"divider": "Uppdelare{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regler för åtkomst",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Kaydet ve Çık",
|
||||
"keepEditing": "Düzenlemeye Devam Et",
|
||||
"closeAnyway": "Her Neyse Kapat",
|
||||
"goToItem": "Öğeye git",
|
||||
"watchFile": "Dosyayı izle",
|
||||
"showMore": "Gelişmiş Seçenekleri Göster",
|
||||
"showLess": "Gelişmiş Seçenekleri Gizle"
|
||||
"showLess": "Gelişmiş Seçenekleri Gizle",
|
||||
"goToSource": "Kaynak Konuma Git",
|
||||
"goToItem": "Öğeye git"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Dosyayı indir",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Hedef klasörü seçin (ardından aşağıya arşiv adını girin):",
|
||||
"archiveName": "Arşiv adı (varsayılan archive.zip):",
|
||||
"archiveCompression": "Sıkıştırma seviyesi (0-9):",
|
||||
"archiveDeleteAfter": "Başarılı bir şekilde oluşturulduktan sonra kaynak dosyaları silme",
|
||||
"archiveSuccess": "Arşiv başarıyla oluşturuldu!",
|
||||
"unarchive": "Arşivi ayıkla",
|
||||
"unarchiveMessage": "Arşivin nereye çıkarılacağını seçin.",
|
||||
"unarchiveDestination": "Özetle:",
|
||||
"unarchiveDeleteAfter": "Çıkarma işleminden sonra arşivi silme",
|
||||
"unarchiveSuccess": "Arşiv başarıyla çıkarıldı!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "İşletim sistemi tarafından bildirilen disk/bölüm boyutundan kullanım çubuğunu görüntüleyin. Bu boyut büyük olasılıkla daha büyük olacaktır ve dizine eklenmemiş dosyaları da içerir.",
|
||||
"usageTextDisplay": "Hangi kullanım metni görüntülenmelidir?",
|
||||
"usageTextIndexed": "endeksli olarak kullanım",
|
||||
"usageTextDisk": "disk/bölüm tarafından bildirilen kullanım"
|
||||
"usageTextDisk": "disk/bölüm tarafından bildirilen kullanım",
|
||||
"dividerNamePlaceholder": "Bölücü metin (bir çizgi göstermek için boş bırakın)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link kopyalandı!"
|
||||
@@ -564,8 +564,9 @@
|
||||
"results": "Sonuçlar{suffix}",
|
||||
"users": "Kullanıcılar{suffix}",
|
||||
"photos": "Fotoğraflar",
|
||||
"audio": "Ses{suffix}",
|
||||
"audios": "Ses Dosyaları{suffix}",
|
||||
"videos": "Videolar{suffix}",
|
||||
"models": "3D Modeller",
|
||||
"documents": "Belgeler{suffix}",
|
||||
"archives": "Arşivler{suffix}",
|
||||
"images": "Görüntüler{suffix}",
|
||||
@@ -648,7 +649,8 @@
|
||||
"space": "Uzay{suffix}",
|
||||
"format": "Biçim{suffix}",
|
||||
"office": "Ofis{suffix}",
|
||||
"show": "Göster{suffix}"
|
||||
"show": "Göster{suffix}",
|
||||
"divider": "Bölücü{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Erişim kuralları",
|
||||
@@ -694,6 +696,12 @@
|
||||
"orange": "turuncu"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Arşiv oluşturma/çıkarma işleminden sonra kaynak dosyaları sil",
|
||||
"deleteAfterArchiveDescription": "Etkinleştirildiğinde, arşivlerin başarıyla oluşturulması/çıkarılmasından sonra kaynak dosyaları siler",
|
||||
"hideFilesInTree": "Ağaç gezintisinde dosyaları gizle",
|
||||
"hideFilesInTreeDescription": "Etkinleştirildiğinde, kenar çubuğu Ağaç Gezintisinde yalnızca klasörler gösterilir",
|
||||
"previewModelsDescription": "3D modeller için küçük resim önizlemelerini göster, devre dışı bırakılırsa genel simge gösterilir",
|
||||
"previewAudiosDescription": "Ses dosyaları için küçük resim önizlemelerini göster, devre dışı bırakılırsa genel simge gösterilir",
|
||||
"setDateFormat": "Tam tarih biçimini ayarlama",
|
||||
"showHiddenFiles": "Gizli dosyaları göster",
|
||||
"showHiddenFilesDescription": "Gizli dosyaları listede gösterin (örn. .thumbnails, ProgramData), ancak dizine eklenecek şekilde yapılandırılmışlarsa yine de manuel olarak erişilebilirler.",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "Зберегти та вийти",
|
||||
"keepEditing": "Продовжуйте редагувати",
|
||||
"closeAnyway": "Все одно зачиняй.",
|
||||
"goToItem": "Перейти до пункту",
|
||||
"watchFile": "Переглянути файл",
|
||||
"showMore": "Показати додаткові параметри",
|
||||
"showLess": "Приховати додаткові параметри"
|
||||
"showLess": "Приховати додаткові параметри",
|
||||
"goToSource": "Перейдіть до Розташування джерела",
|
||||
"goToItem": "Перейти до пункту"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Завантажити файл",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "Виберіть папку призначення (потім введіть ім'я архіву нижче):",
|
||||
"archiveName": "Ім'я архіву (за замовчуванням archive.zip):",
|
||||
"archiveCompression": "Рівень стиснення (0-9):",
|
||||
"archiveDeleteAfter": "Видаліть вихідні файли після успішного створення",
|
||||
"archiveSuccess": "Архів створено успішно!",
|
||||
"unarchive": "Витягнути архів",
|
||||
"unarchiveMessage": "Виберіть, куди розпакувати архів.",
|
||||
"unarchiveDestination": "Витягнути до:",
|
||||
"unarchiveDeleteAfter": "Видалення архіву після розпакування",
|
||||
"unarchiveSuccess": "Архів успішно розпаковано!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "Відображати смужку використання від розміру диска/розділу, про який повідомляє операційна система. Цей розмір, ймовірно, буде більшим і включатиме неіндексовані файли.",
|
||||
"usageTextDisplay": "Який текст використання повинен відображатися?",
|
||||
"usageTextIndexed": "використання в індексованому вигляді",
|
||||
"usageTextDisk": "використання за диском/розділом"
|
||||
"usageTextDisk": "використання за диском/розділом",
|
||||
"dividerNamePlaceholder": "Текст роздільника (залиште порожнім, щоб показати лінію)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Посилання скопійоване!"
|
||||
@@ -550,6 +550,12 @@
|
||||
"orange": "помаранчевий"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "Видаляти вихідні файли після створення/вилучення архівів",
|
||||
"deleteAfterArchiveDescription": "Якщо ввімкнено, видалятиме вихідні файли після успішного створення/вилучення архівів",
|
||||
"hideFilesInTree": "Приховувати файли в дереві навігації",
|
||||
"hideFilesInTreeDescription": "Якщо ввімкнено, у деревоподібній навігації бічної панелі відображаються лише папки",
|
||||
"previewModelsDescription": "Показувати мініатюри для 3D-моделей, якщо вимкнено, буде показано стандартну іконку",
|
||||
"previewAudiosDescription": "Показувати мініатюри для аудіофайлів, якщо вимкнено, буде показано стандартну іконку",
|
||||
"setDateFormat": "Встановіть точний формат дати",
|
||||
"showHiddenFiles": "Показати приховані файли",
|
||||
"showHiddenFilesDescription": "Показувати приховані файли у лістингу (наприклад, .thumbnails, ProgramData), але вони все ще доступні вручну, якщо їх налаштовано на індексування.",
|
||||
@@ -642,7 +648,8 @@
|
||||
"results": "Результати{suffix}",
|
||||
"users": "Користувачі{suffix}",
|
||||
"photos": "Фотографії",
|
||||
"audio": "Аудіо{suffix}",
|
||||
"audios": "Аудіо{suffix}",
|
||||
"models": "3D-моделі",
|
||||
"videos": "Відео{suffix}",
|
||||
"documents": "Документи{suffix}",
|
||||
"archives": "Архіви{suffix}",
|
||||
@@ -726,7 +733,8 @@
|
||||
"space": "Простір{suffix}",
|
||||
"format": "Формат{suffix}",
|
||||
"office": "Офіс{suffix}",
|
||||
"show": "Показати{suffix}"
|
||||
"show": "Показати{suffix}",
|
||||
"divider": "Перегородка{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Правила доступу",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "保存并退出",
|
||||
"keepEditing": "继续编辑",
|
||||
"closeAnyway": "无论如何关闭",
|
||||
"goToItem": "转到项目",
|
||||
"watchFile": "观看文件",
|
||||
"showMore": "显示高级选项",
|
||||
"showLess": "隐藏高级选项"
|
||||
"showLess": "隐藏高级选项",
|
||||
"goToSource": "转到源位置",
|
||||
"goToItem": "转到项目"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下载文件",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "选择目标文件夹(然后在下面输入存档名称):",
|
||||
"archiveName": "存档名称(默认为 archive.zip):",
|
||||
"archiveCompression": "压缩级别(0-9):",
|
||||
"archiveDeleteAfter": "成功创建后删除源文件",
|
||||
"archiveSuccess": "存档创建成功!",
|
||||
"unarchive": "提取档案",
|
||||
"unarchiveMessage": "选择提取存档的位置。",
|
||||
"unarchiveDestination": "摘录至",
|
||||
"unarchiveDeleteAfter": "提取后删除存档",
|
||||
"unarchiveSuccess": "档案提取成功!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "根据操作系统报告的磁盘/分区大小显示用量条。该大小可能更大,包括未编入索引的文件。",
|
||||
"usageTextDisplay": "应显示哪些使用文本?",
|
||||
"usageTextIndexed": "指数化使用",
|
||||
"usageTextDisk": "磁盘/分区报告的使用量"
|
||||
"usageTextDisk": "磁盘/分区报告的使用量",
|
||||
"dividerNamePlaceholder": "分隔线文本(留空以显示一行)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "链接已复制!"
|
||||
@@ -550,6 +550,12 @@
|
||||
"orange": "橘色"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "创建/提取存档后删除源文件",
|
||||
"deleteAfterArchiveDescription": "启用后,将在成功创建/提取存档后删除源文件",
|
||||
"hideFilesInTree": "在树形导航中隐藏文件",
|
||||
"hideFilesInTreeDescription": "启用后,侧边栏树形导航中仅显示文件夹",
|
||||
"previewModelsDescription": "显示3D模型的缩略图预览,如果禁用则显示通用图标",
|
||||
"previewAudiosDescription": "显示音频的缩略图预览,如果禁用则显示通用图标",
|
||||
"setDateFormat": "设置准确的日期格式",
|
||||
"showHiddenFiles": "显示隐藏文件",
|
||||
"showHiddenFilesDescription": "在列表中显示隐藏文件(如 .缩略图、ProgramData),但如果配置为索引,这些文件仍可手动访问。",
|
||||
@@ -642,7 +648,8 @@
|
||||
"results": "成果{suffix}",
|
||||
"users": "用户{suffix}",
|
||||
"photos": "照片",
|
||||
"audio": "音频{suffix}",
|
||||
"audios": "音频{suffix}",
|
||||
"models": "3D模型",
|
||||
"videos": "视频{suffix}",
|
||||
"documents": "文件{suffix}",
|
||||
"archives": "档案{suffix}",
|
||||
@@ -726,7 +733,8 @@
|
||||
"space": "空间{suffix}",
|
||||
"format": "格式{suffix}",
|
||||
"office": "办公室{suffix}",
|
||||
"show": "显示{suffix}"
|
||||
"show": "显示{suffix}",
|
||||
"divider": "分割器{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "访问规则",
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
"saveAndExit": "儲存並離開",
|
||||
"keepEditing": "繼續編輯",
|
||||
"closeAnyway": "無論如何關閉",
|
||||
"goToItem": "前往項目",
|
||||
"watchFile": "觀看檔案",
|
||||
"showMore": "顯示進階選項",
|
||||
"showLess": "隱藏進階選項"
|
||||
"showLess": "隱藏進階選項",
|
||||
"goToSource": "前往來源位置",
|
||||
"goToItem": "前往項目"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下載檔案",
|
||||
@@ -221,12 +222,10 @@
|
||||
"archiveDestination": "選擇目的地資料夾(然後在下方輸入存檔名稱):",
|
||||
"archiveName": "存檔名稱 (預設為 archive.zip):",
|
||||
"archiveCompression": "壓縮等級 (0-9):",
|
||||
"archiveDeleteAfter": "成功建立後刪除原始檔案",
|
||||
"archiveSuccess": "成功建立存檔!",
|
||||
"unarchive": "擷取存檔",
|
||||
"unarchiveMessage": "選擇提取存檔的位置。",
|
||||
"unarchiveDestination": "擷取至:",
|
||||
"unarchiveDeleteAfter": "萃取後刪除存檔",
|
||||
"unarchiveSuccess": "存檔提取成功!"
|
||||
},
|
||||
"search": {
|
||||
@@ -381,7 +380,8 @@
|
||||
"showDiskUsageDescription": "根據作業系統報告的磁碟/磁碟分割大小顯示使用量列。此大小可能較大,並包括未編制索引的檔案。",
|
||||
"usageTextDisplay": "應該顯示哪些使用文字?",
|
||||
"usageTextIndexed": "索引的用途",
|
||||
"usageTextDisk": "磁碟/磁碟分割報告的使用量"
|
||||
"usageTextDisk": "磁碟/磁碟分割報告的使用量",
|
||||
"dividerNamePlaceholder": "分割線文字(留空顯示一行)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "連結已複製!"
|
||||
@@ -519,6 +519,12 @@
|
||||
"error": "錯誤"
|
||||
},
|
||||
"profileSettings": {
|
||||
"deleteAfterArchive": "建立/解壓縮封存檔後刪除來源檔案",
|
||||
"deleteAfterArchiveDescription": "啟用後,將在成功建立/解壓縮封存檔後刪除來源檔案",
|
||||
"hideFilesInTree": "在樹狀導航中隱藏檔案",
|
||||
"hideFilesInTreeDescription": "啟用後,側邊欄樹狀導航中僅顯示資料夾",
|
||||
"previewModelsDescription": "顯示3D模型的縮圖預覽,如果停用則顯示通用圖示",
|
||||
"previewAudiosDescription": "顯示音訊的縮圖預覽,如果停用則顯示通用圖示",
|
||||
"setDateFormat": "設定精確的日期格式",
|
||||
"showHiddenFiles": "顯示隱藏檔案",
|
||||
"showHiddenFilesDescription": "在列出中顯示隱藏的檔案 (例如 .縮圖、ProgramData),但如果設定為索引,這些檔案仍可手動存取。",
|
||||
@@ -642,8 +648,9 @@
|
||||
"results": "結果{suffix}",
|
||||
"users": "使用者{suffix}",
|
||||
"photos": "圖片",
|
||||
"audio": "音訊{suffix}",
|
||||
"audios": "音訊{suffix}",
|
||||
"videos": "影片{suffix}",
|
||||
"models": "3D模型",
|
||||
"documents": "文件{suffix}",
|
||||
"archives": "檔案{suffix}",
|
||||
"images": "圖片{suffix}",
|
||||
@@ -726,7 +733,8 @@
|
||||
"space": "空間{suffix}",
|
||||
"format": "格式{suffix}",
|
||||
"office": "辦公室{suffix}",
|
||||
"show": "顯示{suffix}"
|
||||
"show": "顯示{suffix}",
|
||||
"divider": "分割器{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "存取規則",
|
||||
|
||||
@@ -16,7 +16,8 @@ const titles = {
|
||||
Login: i18n.global.t("general.login"),
|
||||
Share: i18n.global.t("general.share"),
|
||||
Files: i18n.global.t("general.files"),
|
||||
Tools: i18n.global.t("general.tool"),
|
||||
Tools: i18n.global.t("tools.title"),
|
||||
ChildTool: i18n.global.t("tools.title"),
|
||||
Settings: i18n.global.t("general.settings"),
|
||||
ProfileSettings: i18n.global.t("settings.profileSettings"),
|
||||
Shares: i18n.global.t("settings.shareManagement"),
|
||||
@@ -84,7 +85,7 @@ const routes: RouteRecordRaw[] = [
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "ToolsIndex",
|
||||
name: "ChildTool",
|
||||
component: Tools,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -39,7 +39,7 @@ export const getters = {
|
||||
},
|
||||
isPreviewView: () => {
|
||||
const cv = getters.currentView()
|
||||
return cv == 'preview' || cv == 'onlyOfficeEditor' || cv == 'epubViewer' || cv == 'docViewer' || cv == 'editor' || cv == 'markdownViewer' || cv == 'threeJsViewer'
|
||||
return previewViews.includes(cv)
|
||||
},
|
||||
isScrollable: () => {
|
||||
if (getters.currentView() == 'markdownViewer') {
|
||||
@@ -473,14 +473,19 @@ export const getters = {
|
||||
},
|
||||
preview: {
|
||||
video: true,
|
||||
audio: true,
|
||||
image: true,
|
||||
popup: true,
|
||||
models: true,
|
||||
autoplayMedia: true,
|
||||
},
|
||||
disableSettings: true,
|
||||
disableQuickToggles: false,
|
||||
disableSearchOptions: false,
|
||||
deleteWithoutConfirming: false,
|
||||
deleteAfterArchive: true,
|
||||
stickySidebar: true,
|
||||
hideFilesInTree: false,
|
||||
darkMode: true,
|
||||
dateFormat: false,
|
||||
disableViewingExt: "",
|
||||
@@ -542,9 +547,7 @@ export const getters = {
|
||||
}
|
||||
// Match by path instead of route name
|
||||
const tool = tools().find(t => t.path === state.route.path);
|
||||
if (tool === undefined) {
|
||||
return { name: "Tools" };
|
||||
}
|
||||
// Return null when at /tools (list view) to avoid circular component rendering
|
||||
return tool;
|
||||
},
|
||||
permissions: () => {
|
||||
@@ -576,11 +579,13 @@ export const getters = {
|
||||
if (getters.isShare()) {
|
||||
// For shares, use defaults for preview settings (shares don't have per-share preview config)
|
||||
return {
|
||||
audio: state.user?.preview?.audio ?? true,
|
||||
video: state.user?.preview?.video ?? true,
|
||||
image: state.user?.preview?.image ?? true,
|
||||
office: state.user?.preview?.office ?? true,
|
||||
folder: state.user?.preview?.folder ?? true,
|
||||
popup: state.user?.preview?.popup ?? true,
|
||||
models: state.user?.preview?.models ?? true,
|
||||
motionVideoPreview: state.user?.preview?.motionVideoPreview ?? false,
|
||||
disableHideSidebar: state.user?.preview?.disableHideSidebar ?? false,
|
||||
autoplayMedia: state.user?.preview?.autoplayMedia ?? false,
|
||||
@@ -590,11 +595,13 @@ export const getters = {
|
||||
}
|
||||
// For regular users, use their preview settings -- unless is share
|
||||
return {
|
||||
audio: state.user?.preview?.audio ?? true,
|
||||
video: state.user?.preview?.video ?? true,
|
||||
image: state.user?.preview?.image ?? true,
|
||||
office: state.user?.preview?.office ?? true,
|
||||
folder: state.user?.preview?.folder ?? true,
|
||||
popup: state.user?.preview?.popup ?? true,
|
||||
models: state.user?.preview?.models ?? true,
|
||||
motionVideoPreview: state.user?.preview?.motionVideoPreview ?? false,
|
||||
disableHideSidebar: state.user?.preview?.disableHideSidebar ?? false,
|
||||
autoplayMedia: state.user?.preview?.autoplayMedia ?? false,
|
||||
|
||||
@@ -468,6 +468,19 @@ export const mutations = {
|
||||
mutations.setMultiple(false);
|
||||
emitStateChanged();
|
||||
},
|
||||
selectAllItems: () => {
|
||||
if (state.req && state.req.items && state.req.items.length > 0) {
|
||||
// Close the context menu
|
||||
mutations.closeContextMenus();
|
||||
// Clear current selection first
|
||||
mutations.resetSelected();
|
||||
// Add all items from current directory to selection by their indices
|
||||
state.req.items.forEach((item, index) => {
|
||||
mutations.addSelected(index);
|
||||
});
|
||||
mutations.setMultiple(true);
|
||||
}
|
||||
},
|
||||
setLastSelectedIndex: (index) => {
|
||||
if (index === state.lastSelectedIndex) {
|
||||
return;
|
||||
@@ -522,6 +535,7 @@ export const mutations = {
|
||||
"showFirstLogin",
|
||||
"sidebarLinks",
|
||||
"fileLoading",
|
||||
"deleteAfterArchive",
|
||||
].includes(key)
|
||||
);
|
||||
value.id = state.user.id;
|
||||
|
||||
@@ -41,9 +41,11 @@ export const state = reactive({
|
||||
},
|
||||
user: {
|
||||
preview: {
|
||||
audio: true,
|
||||
video: true,
|
||||
image: true,
|
||||
popup: true,
|
||||
models: true,
|
||||
},
|
||||
loginType: "",
|
||||
username: "",
|
||||
@@ -51,6 +53,8 @@ export const state = reactive({
|
||||
gallerySize: 0,
|
||||
singleClick: false,
|
||||
stickySidebar: stickyStartup(),
|
||||
hideFilesInTree: false,
|
||||
deleteAfterArchive: true,
|
||||
locale: detectLocale(), // Default to the locale from moment
|
||||
viewMode: 'normal', // Default to mosaic view
|
||||
showHidden: false, // Default to false, assuming this is a boolean
|
||||
@@ -162,7 +166,7 @@ export const state = reactive({
|
||||
mode: getSidebarMode(),
|
||||
isResizing: false,
|
||||
minWidth: 17, // in em
|
||||
maxWidth: 37.5, // in em
|
||||
maxWidth: 48, // in em
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ export interface ShareInfoObject {
|
||||
export interface UserObject {
|
||||
preview: {
|
||||
video: boolean;
|
||||
audio: boolean;
|
||||
image: boolean;
|
||||
models: boolean;
|
||||
popup: boolean;
|
||||
autoplayMedia?: boolean;
|
||||
defaultMediaPlayer?: boolean;
|
||||
@@ -53,6 +55,8 @@ export interface UserObject {
|
||||
gallerySize: number;
|
||||
singleClick: boolean;
|
||||
stickySidebar: boolean;
|
||||
hideFilesInTree: boolean,
|
||||
deleteAfterArchive: boolean,
|
||||
locale: string;
|
||||
viewMode: string;
|
||||
showHidden: boolean;
|
||||
|
||||
@@ -9,6 +9,13 @@ const RAW_IMAGE_MIME_TYPES = new Set([
|
||||
"image/x-phaseone-iiq", "image/x-kodak-nkc", "image/x-red-r3d",
|
||||
]);
|
||||
|
||||
const EBOOK_MIME_TYPES = new Set([
|
||||
"application/epub+zip", "application/x-mobipocket-ebook", "application/vnd.amazon.ebook",
|
||||
"application/x-fictionbook+xml", "application/x-fb2", "application/x-cbr", "application/x-cbz",
|
||||
"application/x-cb7", "application/x-cbt", "application/vnd.comicbook+zip",
|
||||
"application/vnd.comicbook-rar", "application/x-kindle", "application/x-azw"
|
||||
]);
|
||||
|
||||
export function isRawImageMimeType(mimeType) {
|
||||
return typeof mimeType === "string" && RAW_IMAGE_MIME_TYPES.has(mimeType);
|
||||
}
|
||||
@@ -29,6 +36,14 @@ export function getTypeInfo(mimeType) {
|
||||
};
|
||||
}
|
||||
|
||||
if (EBOOK_MIME_TYPES.has(mimeType)) {
|
||||
return {
|
||||
classes: "brown-icons material-icons",
|
||||
materialIcon: "menu_book",
|
||||
simpleType: "ebook",
|
||||
};
|
||||
}
|
||||
|
||||
if (mimeType.startsWith("image/")) {
|
||||
return {
|
||||
classes: "orange-icons material-icons",
|
||||
@@ -61,7 +76,7 @@ export function getTypeInfo(mimeType) {
|
||||
|
||||
if (mimeType == "file_download") {
|
||||
return {
|
||||
classes: "material-icons simple-icons",
|
||||
classes: "material-icons",
|
||||
materialIcon: "file_download",
|
||||
simpleType: "file_download",
|
||||
};
|
||||
|
||||
@@ -214,7 +214,9 @@ export function goToItem(source, path, previousHistoryItem, newTab = false) {
|
||||
let newPath = encodedPath(path);
|
||||
let fullPath = `/files/${encodeURIComponent(source)}${newPath}`;
|
||||
if (newTab) {
|
||||
window.open(fullPath, '_blank');
|
||||
// Use absolute URL for new tab to ensure proper navigation
|
||||
const absoluteUrl = `${window.location.origin}${globalVars.baseURL}${fullPath.startsWith('/') ? fullPath.slice(1) : fullPath}`;
|
||||
window.open(absoluteUrl, '_blank');
|
||||
return;
|
||||
}
|
||||
mutations.setPreviousHistoryItem(previousHistoryItem);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<Notifications />
|
||||
<Toast :toasts="toasts" />
|
||||
<StatusBar :class="{ moveWithSidebar: moveWithSidebar.shouldMove }" />
|
||||
<ContextMenu v-if="showContextMenu" v-bind="contextMenuProps"></ContextMenu>
|
||||
<ContextMenu v-bind="contextMenuProps"></ContextMenu>
|
||||
<Tooltip />
|
||||
<NextPrevious />
|
||||
<PopupPreview v-if="popupEnabled" />
|
||||
@@ -141,10 +141,6 @@ export default {
|
||||
currentView() {
|
||||
return getters.currentView();
|
||||
},
|
||||
showContextMenu() {
|
||||
// for now lets disable for tools view
|
||||
return getters.currentView() != "tools"
|
||||
},
|
||||
contextMenuProps() {
|
||||
const prompt = getters.currentPrompt();
|
||||
if (prompt && prompt.name === "ContextMenu") {
|
||||
@@ -171,7 +167,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
const currentView = getters.currentView()
|
||||
mutations.setMultiple(false);
|
||||
const currentPrompt = getters.currentPromptName();
|
||||
if (currentPrompt !== "success" && currentPrompt !== "generic") {
|
||||
mutations.closeTopHover();
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Dynamically render the selected tool component -->
|
||||
<component v-else-if="currentTool" :is="currentTool.component" />
|
||||
<component v-else-if="currentTool" :is="currentTool.component" :key="$route.params.toolName" />
|
||||
|
||||
<!-- Show error if tool not found -->
|
||||
<div v-else class="tool-not-found">
|
||||
|
||||
@@ -116,7 +116,7 @@ export default {
|
||||
return getters.isShare();
|
||||
},
|
||||
noItems() {
|
||||
return !state.contextMenuHasItems;
|
||||
return !state.contextMenuHasItems && !getters.isPreviewView();
|
||||
},
|
||||
showEdit() {
|
||||
return window.location.hash != "#edit" && state.user.permissions.modify;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, watch } from "vue";
|
||||
import ePub, { type Book, type Rendition } from "epubjs";
|
||||
import { state, mutations, getters } from "@/store"; // Assuming your store setup
|
||||
import { resourcesApi } from "@/api";
|
||||
@@ -68,6 +68,12 @@ export default defineComponent({
|
||||
// 4. Display the rendered book
|
||||
await this.rendition.display();
|
||||
|
||||
this.applyTheme(getters.isDarkMode());
|
||||
|
||||
watch(() => getters.isDarkMode(), (isDark) => {
|
||||
this.applyTheme(isDark);
|
||||
});
|
||||
|
||||
// Set flags to show the book and trigger animations
|
||||
this.isReady = true;
|
||||
setTimeout(() => {
|
||||
@@ -84,6 +90,20 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
applyTheme(isDark: boolean) {
|
||||
if (!this.rendition) return;
|
||||
if (isDark) {
|
||||
this.rendition.themes.default({
|
||||
body: { color: "#fff !important" },
|
||||
a: { color: "#bb86fc !important" },
|
||||
});
|
||||
} else {
|
||||
this.rendition.themes.default({
|
||||
body: { color: "#000 !important" },
|
||||
a: { color: "#6200ee !important" },
|
||||
});
|
||||
}
|
||||
},
|
||||
// Navigate to the next page
|
||||
nextPage() {
|
||||
this.rendition?.next();
|
||||
@@ -111,7 +131,7 @@ export default defineComponent({
|
||||
.epub-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--surfaceSecondary); /* A light background for the reader */
|
||||
background-color: var(--background); /* background for the reader */
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@@ -121,7 +141,7 @@ export default defineComponent({
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: 1.2em;
|
||||
color: #6c757d;
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
/* The viewer must have a defined height for epub.js to work */
|
||||
@@ -143,19 +163,25 @@ export default defineComponent({
|
||||
z-index: 1001; /* Ensure controls are on top */
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
background-color: var(--surfaceSecondary);
|
||||
padding: 0.5em;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 1.5em;
|
||||
color: #343a40;
|
||||
color: var(--textPrimary);
|
||||
cursor: pointer;
|
||||
padding: 0.25em 1em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background-color: var(--alt-background);
|
||||
}
|
||||
|
||||
/* Copied directly from your example */
|
||||
@@ -178,5 +204,6 @@ export default defineComponent({
|
||||
font-size: 2em;
|
||||
padding-right: 1em;
|
||||
padding-left: 1em;
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
</style>
|
||||
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user