Compare commits

...
Author SHA1 Message Date
Graham Steffaniak e8a95cb3cf updated linting testing 2025-09-09 18:36:13 -05:00
Graham Steffaniak 5e79faefa1 updated compilation 2025-09-09 18:29:56 -05:00
Graham Steffaniak 92909a7403 fix merge issues 2025-09-09 18:26:40 -05:00
Graham Steffaniak 2385994736 Merge branch 'fix-issues-features' of github.com:gtsteffaniak/filebrowser into fix-issues-features 2025-09-09 18:24:06 -05:00
Graham Steffaniak b0df91cd45 updated 2025-09-09 18:22:40 -05:00
Graham Steffaniak 3330468c1b fix build 2025-09-09 16:12:05 -05:00
Graham Steffaniak 8ef0435602 updated 2025-09-09 16:12:05 -05:00
Graham Steffaniak 32cbec011a fix issues features 2025-09-09 16:12:05 -05:00
Graham Steffaniak e6a8d090ad updated debugging and scoped path (#1198) 2025-09-09 16:12:05 -05:00
Graham Steffaniak 527d1ec810 feat: remember view settings per folder (#1196) 2025-09-09 16:12:05 -05:00
Graham Steffaniak 586a76c5ca fix build 2025-09-09 16:04:19 -05:00
Graham Steffaniak 5be126dc47 updated 2025-09-09 15:51:44 -05:00
Graham Steffaniak b8d3b6aadc fix issues features 2025-09-09 15:35:03 -05:00
Graham Steffaniak 2d2a3d7707 updated debugging and scoped path (#1198) 2025-09-09 12:21:16 -05:00
Graham Steffaniak 7baf2653e0 feat: remember view settings per folder (#1196) 2025-09-08 18:31:00 -05:00
256f4ca0b8 Beta/v0.8.4 (#1187)
Co-authored-by: Max <mail@kjmax.com>
Co-authored-by: Kurami <kurami32_kbm@proton.me>
2025-09-05 18:56:18 -05:00
Graham Steffaniak 0f5c81a19e updated (#1186) 2025-09-05 14:31:11 -05:00
Graham Steffaniak 5c91eebf2a updated with show running config (#1185) 2025-09-05 13:40:07 -05:00
Kurami 59f95e9f22 Add audio metadata on audio media player (#1182) 2025-09-04 20:04:21 -05:00
Graham Steffaniak d4977713e7 onlyoffice share support (#1181) 2025-09-03 19:32:02 -05:00
Graham Steffaniak 99786eafde editor reverted changes (#1180) 2025-09-03 14:00:53 -05:00
Graham Steffaniak cbca224f69 updated with changes for share (#1177) 2025-09-03 10:10:55 -05:00
Graham Steffaniak d9c465bfad fixed help and rename prompt (#1176) 2025-09-02 19:04:02 -05:00
KuramiandGraham Steffaniak 1a36bc7336 Small fixes / additions on the media player (#1173)
Co-authored-by: Graham Steffaniak <42989099+gtsteffaniak@users.noreply.github.com>
2025-09-02 15:43:22 -05:00
Graham Steffaniak ef416956ad updated linting 2025-09-02 13:17:46 -05:00
Graham Steffaniak d8b38de2d7 updated with a few more changes 2025-09-01 17:42:50 -05:00
Graham Steffaniak b6e70ffa6d State management (#1169) 2025-09-01 15:35:21 -05:00
Graham Steffaniak 5dc8da5064 updated with small bugfix 2025-08-29 18:54:52 -05:00
Graham Steffaniak 5967422335 Update video player embedded (#1161) 2025-08-29 18:51:48 -05:00
KuramiandGraham Steffaniak a933d7ecbf Custom Media Player (#1160)
Co-authored-by: Graham Steffaniak <42989099+gtsteffaniak@users.noreply.github.com>
2025-08-29 16:45:10 -05:00
Graham Steffaniak 2b61350102 access management changes (#1158) 2025-08-29 13:27:35 -05:00
Graham SteffaniakandMax 07601bb075 Feature/embedded subtitles (#1157)
Co-authored-by: Max <mail@kjmax.com>
2025-08-29 12:03:29 -05:00
MaxandGraham Steffaniak 4cfd7e00b4 Embedded subtitles support in video files (#1072)
Co-authored-by: Graham Steffaniak <42989099+gtsteffaniak@users.noreply.github.com>
2025-08-29 11:32:06 -05:00
Graham Steffaniak 831708fe65 Add go tool (#1154) 2025-08-28 18:26:29 -05:00
Graham Steffaniak 50bd853266 Beta/v0.8.3 (#1150) 2025-08-28 15:04:08 -05:00
134 changed files with 8593 additions and 3566 deletions

No files matched your search

+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: dist
path: frontend/dist
path: backend/http/dist
build_backend:
name: Build backend
runs-on: ubuntu-latest
+53 -1
View File
@@ -2,6 +2,58 @@
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).
## v0.8.4-beta
**New Features**:
- Remember folder view type (and sorting) for each folder #966
- heic is supported in preview and viewer (via conversion) when on non-safari browser #1191
**Notes**:
**BugFixes**:
- Access Management: issue with access settings #1195
- fix shutdown panic related to sse connection
## v0.8.4-beta
**New Features**:
- New media player styles and features
- Custom Media Player: enhanced media player using plyr thanks @Kurami32 (see #1160)
- Custom Media Player: also adds support for metadata
- added embeded video subtitle support (for both native and custom player). @maxbin123 #1072 #1157
- Users can disable the customer player and opt of native in profile settings.
- Option to disable backend update check via `server.disableUpdateCheck` #1134
- added `frontend.favicon` and `frontend.description` for html overrides
- onlyoffice is now supported in shares. Both viewing and editing can be configured per-share.
- Added only office debug view and wiki to assist with debugging issues #1068 #911 #1074
- Dark mode enforcement possible for shared links #1029
- added `System & Admin` section to settings
- includes a new config viewer to see current running config (hides secrets) #838
- added `server.minSearchLength` to allow adjusting the length requirement for search #1174
**Notes**:
- access management: specific folders/files with access are shown instead permission denied for parent folder
- navigation no longer appends last location hash which should fix some unwanted navation behavior #1070
- altered the context menu style and behavior.
- documentation update: comma or Space separated extensions #1138
- Files and folders can be created with "/" or "\" on the name #1126
- Share management should not be allowed without authentication #1163
- Question about customizing session timeout #1184
**BugFixes**:
- access management: delay showing rule changes in the list fixed. #1131
- Color names are not localized #1159
- rename issues #1170 #1171
- some shortcuts not working #1056
- Can't copy/paste text on mobile #1168
- Can't change between images inside of the share image viewer. #1144
- fixed and updated translations with variables always showing english.
## v0.8.3-beta
**BugFixes**:
- fixed search bar style bug in mobile #1147
## v0.8.2-beta
**New Features**:
@@ -13,7 +65,7 @@ All notable changes to this project will be documented in this file. For commit
**Notes**:
- 8.0 ffmpeg version bundled with docker
- go 1.25 upgrade with green tea GC enabled
- totp secrets accept non-secure strings, only throwing warning
- totp secrets accept non-secure strings, only throwing warning
- adjusted download limit so it also counts viewing text "content" of files (like in editor). You can also "disable file viewing" to stop the editor from showing. lower quality file image previews are not counted as downloads.
- updated invalid share message to be more clear https://github.com/gtsteffaniak/filebrowser/issues/1120
+14 -25
View File
@@ -4,19 +4,19 @@ Thank you for your interest in contributing to FileBrowser Quantum! This guide w
## Prerequisites
- **Go 1.24+** (see `backend/go.mod`)
- **Go 1.25+** (see `backend/go.mod`)
- **Node.js 18.0.0+** with npm 7.0.0+ (see `frontend/package.json`)
- **Docker** (optional, for containerized development)
- **Git**
### Required Tools
### Optional Tools
- **ffmpeg**: Required for video thumbnail generation
- **ffmpeg**: For media features (subtitles, thumbnails, heic)
- Ubuntu/Debian: `sudo apt-get install ffmpeg`
- macOS: `brew install ffmpeg`
- Windows: Download from [ffmpeg.org](https://ffmpeg.org/download.html)
- **mupdf-tools**: Required for PDF preview generation
- **mupdf-tools**: For PDF preview generation
- Ubuntu/Debian: `sudo apt-get install mupdf-tools`
- macOS: `brew install mupdf-tools`
- Windows: Download from [mupdf.com](https://mupdf.com/downloads/)
@@ -31,8 +31,8 @@ cd filebrowser
# 2. Initial setup - installs dependencies and creates test config
make setup
# 3. Run in development mode
make run
# 3. Run in development mode with hot-reloading
make dev
```
`make setup` installs all dependencies and creates a test configuration file.
@@ -52,14 +52,14 @@ make run
- **Framework**: Vue 3 + Vite + TypeScript
- **State**: Custom store in `frontend/src/store/`
- **API Client**: Axios-based in `frontend/src/api/`
- **i18n**: 25+ languages with English as master
- **i18n**: 25+ languages with English as main
- **Components**: Feature-based organization
## Development Commands
### Essential Commands
```bash
make run # Start development server
make dev # Start development server with hot-reloading
make test # Run all tests
make lint # Check code quality
make check-all # Lint + tests
@@ -70,12 +70,12 @@ make build # Build Docker image
```
### Frontend Development
Note: consider using make commands above instead.
```bash
cd frontend
npm run dev # Dev server with hot reload
npm run lint:fix # Auto-fix linting issues
npm run typecheck # TypeScript validation
npm run i18n:sync # Sync translations
npm run i18n:sync # Sync translations changes
```
## Testing
@@ -128,12 +128,11 @@ make build-backend # Build Go binary with embedded assets
### Docker
```bash
make build # Full image with ffmpeg (~200MB)
make run-proxy # Docker Compose with nginx proxy
make build # Full image with ffmpeg and muPDF
```
### Configuration
- **Test Config**: `backend/test_config.yaml` Auto-generated after running `make setup`
- **Test Config**: `backend/test_config.yaml` Auto-generated after running `make setup`
## Contributing
@@ -141,7 +140,7 @@ make run-proxy # Docker Compose with nginx proxy
1. Fork and create a feature branch
2. Make your changes following the code standards
3. Run `make run` to build and run with your changes
3. Run `make dev` to build and run with your changes. Supports hot-reloading frontend and backend changes.
4. When ready, run `make check-all` to verify tests and linting
5. Submit PR with a clear description
@@ -175,16 +174,6 @@ cd frontend && rm -rf node_modules && npm install
cd backend && go mod tidy && go clean -modcache
```
**Debugging:**
```bash
# Backend with debug logs
cd backend
FILEBROWSER_NO_EMBEDDED=true go run -tags mupdf . -c test_config.yaml --log-level debug
# API documentation
# Visit /swagger endpoint when running
```
**Authentication Issues:**
Always first enable debug logging in your config file if you have issues.
+3 -1
View File
@@ -16,6 +16,9 @@ RUN upx filebrowser
FROM node:lts-slim AS nbuild
WORKDIR /app
COPY ./frontend/package.json ./
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& git config --global url."https://github.com/".insteadOf "git@github.com:" \
&& git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
RUN npm i --maxsockets 1
COPY ./frontend/ ./
RUN npm run build-docker
@@ -24,7 +27,6 @@ FROM alpine:latest
COPY --from=ffmpeg [ "/ffmpeg", "/ffprobe", "/usr/local/bin/" ]
ENV FILEBROWSER_FFMPEG_PATH="/usr/local/bin/"
ENV FILEBROWSER_DATABASE="/home/filebrowser/data/database.db"
ENV FILEBROWSER_NO_EMBEDED="true"
ENV PATH="$PATH:/home/filebrowser"
RUN apk --no-cache add ca-certificates mailcap tzdata
RUN adduser -D -s /bin/true -u 1000 filebrowser
+1 -1
View File
@@ -15,4 +15,4 @@ COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
RUN ./filebrowser set -u testuser1,testuser1
RUN FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
+1 -1
View File
@@ -13,4 +13,4 @@ RUN mkdir -p ./http/dist
COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
RUN FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
+1 -1
View File
@@ -10,4 +10,4 @@ COPY ["./backend/filebrowser", "./"]
COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
WORKDIR /app/frontend
RUN node mock-oidc-server.js & sleep 2 && cd ../backend/ && FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
RUN node mock-oidc-server.js & sleep 2 && cd ../backend/ && ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
+1 -1
View File
@@ -11,4 +11,4 @@ RUN apt update && apt install nginx -y
RUN mv default.conf /etc/nginx/conf.d/default.conf
RUN mv .htpasswd /etc/nginx/conf.d/.htpasswd
RUN sed -i 's/filebrowser/localhost/g' /etc/nginx/conf.d/default.conf
RUN nginx & FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
RUN nginx & ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
+1 -1
View File
@@ -14,4 +14,4 @@ RUN mkdir -p ./http/dist
COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
RUN FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
+3 -1
View File
@@ -14,12 +14,14 @@ RUN upx filebrowser
FROM node:lts-slim AS nbuild
WORKDIR /app
COPY ./frontend/package.json ./
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& git config --global url."https://github.com/".insteadOf "git@github.com:" \
&& git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
RUN npm i --maxsockets 1
COPY ./frontend/ ./
RUN npm run build-docker
FROM alpine:latest
ENV FILEBROWSER_NO_EMBEDED="true"
ENV FILEBROWSER_DATABASE="/home/filebrowser/data/database.db"
ENV PATH="$PATH:/home/filebrowser"
RUN apk --no-cache add ca-certificates mailcap tzdata
@@ -11,7 +11,7 @@ import { defineConfig, devices } from "@playwright/test";
*/
export default defineConfig({
globalSetup: "./tests/playwright/proxy-setup.ts",
timeout: 3000,
timeout: 5000,
testDir: "./tests/playwright/proxy",
/* Run tests in files in parallel */
fullyParallel: false,
+17
View File
@@ -0,0 +1,17 @@
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/filebrowser --tags=mupdf --ldflags=\"-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'\" ."
bin = "tmp/filebrowser"
full_bin = "FILEBROWSER_DEVMODE=true CGO_ENABLED=1 ./tmp/filebrowser -c test_config.yaml"
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["tmp", "vendor", "http/dist", "http/embed", "swagger/docs"]
log = "air.log"
delay = 1000 # ms
[log]
time = true
[misc]
clean_on_exit = true
+5
View File
@@ -0,0 +1,5 @@
tmp/
air.log
filebrowser
generated.yaml
test_config.yaml
+61 -25
View File
@@ -28,18 +28,16 @@ import (
"github.com/gtsteffaniak/go-logger/logger"
)
func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error) {
response := iteminfo.ExtendedFileInfo{}
func FileInfoFaster(opts iteminfo.FileOptions) (*iteminfo.ExtendedFileInfo, error) {
response := &iteminfo.ExtendedFileInfo{}
index := indexing.GetIndex(opts.Source)
if index == nil {
return response, fmt.Errorf("could not get index: %v ", opts.Source)
}
if opts.Access != nil && !opts.Access.Permitted(index.Path, opts.Path, opts.Username) {
return response, errors.ErrPermissionDenied
}
realPath, isDir, err := index.GetRealPath(opts.Path)
if err != nil {
return response, err
return response, fmt.Errorf("could not get real path for requested path: %v", opts.Path)
}
opts.IsDir = isDir
var info *iteminfo.FileInfo
@@ -60,33 +58,71 @@ func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
}
}
if opts.Content {
if info.Size < 20*1024*1024 { // 20 megabytes in bytes
content, err := getContent(realPath)
if err != nil {
logger.Debugf("could not get content for file: "+info.Path, info.Name, err)
return response, err
}
response.Content = content
} else {
logger.Debug("skipping large text file contents (20MB limit): "+info.Path, info.Name)
}
}
response.FileInfo = *info
response.RealPath = realPath
response.Source = index.Name
response.Source = opts.Source
if opts.Access != nil && !opts.Access.Permitted(index.Path, opts.Path, opts.Username) {
// check if any subpath is permitted
// keep track of permitted paths and only show them at the end
subFolders := info.Folders
subFiles := info.Files
response.Folders = make([]iteminfo.ItemInfo, 0)
response.Files = make([]iteminfo.ItemInfo, 0)
hasPermittedPaths := false
for _, subFolder := range subFolders {
indexPath := info.Path + subFolder.Name
if opts.Access.Permitted(index.Path, indexPath, opts.Username) {
hasPermittedPaths = true
response.Folders = append(response.Folders, subFolder)
}
}
for _, subFile := range subFiles {
indexPath := info.Path + subFile.Name
if opts.Access.Permitted(index.Path, indexPath, opts.Username) {
hasPermittedPaths = true
response.Files = append(response.Files, subFile)
}
}
if !hasPermittedPaths {
return response, errors.ErrPermissionDenied
}
}
if opts.Content {
processContent(response, index)
}
if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && iteminfo.IsOnlyOffice(info.Name) {
response.OnlyOfficeId = generateOfficeId(realPath)
}
if strings.HasPrefix(info.Type, "video") {
parentInfo, exists := index.GetReducedMetadata(filepath.Dir(info.Path), true)
if exists {
response.DetectSubtitles(parentInfo)
}
}
return response, nil
}
func processContent(info *iteminfo.ExtendedFileInfo, idx *indexing.Index) {
isVideo := strings.HasPrefix(info.Type, "video")
if isVideo {
parentInfo, exists := idx.GetReducedMetadata(filepath.Dir(info.Path), true)
if exists {
info.DetectSubtitles(parentInfo)
err := info.LoadSubtitleContent()
if err != nil {
logger.Debug("failed to load subtitle content: " + err.Error())
}
}
return
}
if info.Size < 20*1024*1024 { // 20 megabytes in bytes
content, err := getContent(info.RealPath)
if err != nil {
logger.Debugf("could not get content for file: "+info.RealPath, info.Name, err)
return
}
info.Content = content
} else {
logger.Debug("skipping large text file contents (20MB limit): "+info.Path, info.Name)
}
}
func generateOfficeId(realPath string) string {
key, ok := utils.OnlyOfficeCache.Get(realPath).(string)
if !ok {
+7 -5
View File
@@ -54,12 +54,14 @@ func StartFilebrowser() {
if !keepGoing {
return
}
info, _ := utils.CheckForUpdates()
if info.LatestVersion != "" {
logger.Infof("A new version is available: %s (current: %s)", info.LatestVersion, info.CurrentVersion)
logger.Infof("Release notes: %s", info.ReleaseNotes)
if !settings.Config.Server.DisableUpdateCheck {
info, _ := utils.CheckForUpdates()
if info.LatestVersion != "" {
logger.Infof("A new version is available: %s (current: %s)", info.LatestVersion, info.CurrentVersion)
logger.Infof("Release notes: %s", info.ReleaseNotes)
}
go utils.StartCheckForUpdates()
}
go utils.StartCheckForUpdates()
// Create context and channels for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
+7 -7
View File
@@ -12,12 +12,12 @@ import (
)
type Auth struct {
TokenExpirationHours int `json:"tokenExpirationHours"` // the number of hours until the token expires. Default is 2 hours.
TokenExpirationHours int `json:"tokenExpirationHours"` // time in hours each web UI session token is valid for. Default is 2 hours.
Methods LoginMethods `json:"methods"`
Key string `json:"key"` // the key used to sign the JWT tokens. If not set, a random key will be generated.
AdminUsername string `json:"adminUsername"` // the username of the admin user. If not set, the default is "admin".
AdminPassword string `json:"adminPassword"` // the password of the admin user. If not set, the default is "admin".
TotpSecret string `json:"totpSecret"` // secret used to encrypt TOTP secrets
Key string `json:"key"` // secret: the key used to sign the JWT tokens. If not set, a random key will be generated.
AdminUsername string `json:"adminUsername"` // secret: the username of the admin user. If not set, the default is "admin".
AdminPassword string `json:"adminPassword"` // secret: the password of the admin user. If not set, the default is "admin".
TotpSecret string `json:"totpSecret"` // secret: secret used to encrypt TOTP secrets
AuthMethods []string `json:"-"`
}
@@ -52,8 +52,8 @@ type Recaptcha struct {
// OpenID OAuth2.0
type OidcConfig struct {
Enabled bool `json:"enabled"` // whether to enable OIDC authentication
ClientID string `json:"clientId"` // client id of the OIDC application
ClientSecret string `json:"clientSecret"` // client secret of the OIDC application
ClientID string `json:"clientId"` // secret: client id of the OIDC application
ClientSecret string `json:"clientSecret"` // secret: client secret of the OIDC application
IssuerUrl string `json:"issuerUrl"` // authorization URL of the OIDC provider
Scopes string `json:"scopes"` // scopes to request from the OIDC provider
UserIdentifier string `json:"userIdentifier"` // the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone"
+66 -3
View File
@@ -48,6 +48,9 @@ func Initialize(configFile string) {
}
func setupFrontend(generate bool) {
if Config.Server.MinSearchLength == 0 {
Config.Server.MinSearchLength = 3
}
if !Config.Frontend.DisableDefaultLinks {
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
Text: fmt.Sprintf("(%v)", version.Version),
@@ -56,12 +59,15 @@ func setupFrontend(generate bool) {
})
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
Text: "Help",
Url: "https://github.com/gtsteffaniak/filebrowser/wiki",
Url: "help prompt",
})
}
if Config.Frontend.Description == "" {
Config.Frontend.Description = "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server"
}
Config.Frontend.Styling.LightBackground = FallbackColor(Config.Frontend.Styling.LightBackground, "#f5f5f5")
Config.Frontend.Styling.DarkBackground = FallbackColor(Config.Frontend.Styling.DarkBackground, "#141D24")
Config.Frontend.Styling.CustomCSS = readCustomCSS(Config.Frontend.Styling.CustomCSS)
Config.Frontend.Styling.CustomCSSRaw = readCustomCSS(Config.Frontend.Styling.CustomCSS)
Config.Frontend.Styling.CustomThemeOptions = map[string]CustomTheme{}
Config.Frontend.Styling.CustomThemes = map[string]CustomTheme{}
for name, theme := range Config.Frontend.Styling.CustomThemes {
@@ -85,6 +91,9 @@ func setupFrontend(generate bool) {
if !ok {
addCustomTheme("default", "The default theme", "")
}
// Load custom favicon if configured
loadCustomFavicon()
}
func getRealPath(path string) string {
@@ -225,8 +234,14 @@ func setupLogging() {
}
}
for _, logConfig := range Config.Server.Logging {
// Enable debug logging automatically in dev mode
levels := logConfig.Levels
if os.Getenv("FILEBROWSER_DEVMODE") == "true" {
levels = "info|warning|error|debug"
}
logConfig := logger.JsonConfig{
Levels: logConfig.Levels,
Levels: levels,
ApiLevels: logConfig.ApiLevels,
Output: logConfig.Output,
Utc: logConfig.Utc,
@@ -505,6 +520,54 @@ func GetSources(u *users.User) []string {
return sources
}
func loadCustomFavicon() {
// Check if a custom favicon path is configured
if Config.Frontend.Favicon == "" {
logger.Debug("No custom favicon configured, using default")
return
}
// Get absolute path for the favicon
faviconPath, err := filepath.Abs(Config.Frontend.Favicon)
if err != nil {
logger.Warningf("Could not resolve favicon path '%v': %v", Config.Frontend.Favicon, err)
Config.Frontend.Favicon = "" // Unset invalid path
return
}
// Check if the favicon file exists and get info
stat, err := os.Stat(faviconPath)
if err != nil {
logger.Warningf("Could not access custom favicon file '%v': %v", faviconPath, err)
Config.Frontend.Favicon = "" // Unset invalid path
return
}
// Check file size (limit to 1MB for security)
const maxFaviconSize = 1024 * 1024 // 1MB
if stat.Size() > maxFaviconSize {
logger.Warningf("Favicon file '%v' is too large (%d bytes), maximum allowed is %d bytes", faviconPath, stat.Size(), maxFaviconSize)
Config.Frontend.Favicon = "" // Unset invalid path
return
}
// Validate file format based on extension
ext := strings.ToLower(filepath.Ext(faviconPath))
switch ext {
case ".ico", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp":
// Valid favicon formats
default:
logger.Warningf("Unsupported favicon format '%v', supported formats: .ico, .png, .jpg, .gif, .svg, .webp", ext)
Config.Frontend.Favicon = "" // Unset invalid path
return
}
// Update to absolute path and mark as valid
Config.Frontend.Favicon = faviconPath
logger.Infof("Successfully validated custom favicon at '%v' (%d bytes, %s)", faviconPath, stat.Size(), ext)
}
func modifyExcludeInclude(config *Source) {
normalize := func(s []string, checkExists bool) {
for i, v := range s {
+289 -42
View File
@@ -14,19 +14,44 @@ import (
"gopkg.in/yaml.v3"
)
// commentsMap[typeName][fieldName] = combined doc+inline comment text
type commentsMap map[string]map[string]string
// CommentsMap[typeName][fieldName] = combined doc+inline comment text
type CommentsMap map[string]map[string]string
// collectComments parses all Go source in the directory of srcPath and returns commentsMap.
func collectComments(srcPath string) (commentsMap, error) {
// SecretFieldsMap[typeName][fieldName] = true if field should be redacted
type SecretFieldsMap map[string]map[string]bool
// DeprecatedFieldsMap[typeName][fieldName] = true if field is deprecated
type DeprecatedFieldsMap map[string]map[string]bool
// getStringStyle determines whether a string should be quoted in YAML
func getStringStyle(value string) yaml.Style {
// Always quote all strings for consistency
return yaml.DoubleQuotedStyle
}
// CollectComments parses all Go source in the directory of srcPath and returns CommentsMap.
func CollectComments(srcPath string) (CommentsMap, error) {
comments, _, _, err := CollectCommentsAndSecrets(srcPath)
return comments, err
}
// CollectCommentsAndSecrets parses all Go source and returns comments, secrets, and deprecated field mappings
func CollectCommentsAndSecrets(srcPath string) (CommentsMap, SecretFieldsMap, DeprecatedFieldsMap, error) {
// parse entire package so we capture comments on all types
dir := filepath.Dir(srcPath)
dir := srcPath
if filepath.IsAbs(srcPath) {
// If it's an absolute path to a file, get the directory
dir = filepath.Dir(srcPath)
}
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments)
if err != nil {
return nil, err
return nil, nil, nil, err
}
out := make(commentsMap)
comments := make(CommentsMap)
secrets := make(SecretFieldsMap)
deprecated := make(DeprecatedFieldsMap)
for _, pkg := range pkgs {
for _, file := range pkg.Files {
for _, decl := range file.Decls {
@@ -43,48 +68,81 @@ func collectComments(srcPath string) (commentsMap, error) {
if !ok {
continue
}
m := make(map[string]string)
out[ts.Name.Name] = m
typeName := ts.Name.Name
commentMap := make(map[string]string)
secretMap := make(map[string]bool)
deprecatedMap := make(map[string]bool)
comments[typeName] = commentMap
secrets[typeName] = secretMap
deprecated[typeName] = deprecatedMap
for _, field := range st.Fields.List {
if len(field.Names) == 0 {
continue
}
name := field.Names[0].Name
var parts []string
var fullComment string
if field.Doc != nil {
parts = append(parts, strings.TrimSpace(field.Doc.Text()))
docText := strings.TrimSpace(field.Doc.Text())
parts = append(parts, docText)
fullComment += docText + " "
}
if field.Comment != nil {
parts = append(parts, strings.TrimSpace(field.Comment.Text()))
commentText := strings.TrimSpace(field.Comment.Text())
parts = append(parts, commentText)
fullComment += commentText
}
if len(parts) > 0 {
m[name] = strings.Join(parts, " : ")
commentMap[name] = strings.Join(parts, " : ")
}
// Check if field should be treated as secret
if strings.Contains(strings.ToLower(fullComment), "secret:") {
secretMap[name] = true
}
// Check if field should be treated as deprecated
if strings.Contains(strings.ToLower(fullComment), "deprecated:") {
deprecatedMap[name] = true
}
}
}
}
}
}
return out, nil
return comments, secrets, deprecated, nil
}
// buildNode constructs a yaml.Node for any Go value, injecting comments on struct fields.
func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) {
// BuildNode constructs a yaml.Node for any Go value, injecting comments on struct fields.
func BuildNode(v reflect.Value, comm CommentsMap) (*yaml.Node, error) {
return buildNodeWithDefaults(v, comm, reflect.Value{}, SecretFieldsMap{}, DeprecatedFieldsMap{})
}
// buildNodeWithDefaults constructs a yaml.Node for any Go value, skipping fields that match defaults, redacting secrets, and filtering deprecated fields
func buildNodeWithDefaults(v reflect.Value, comm CommentsMap, defaults reflect.Value, secrets SecretFieldsMap, deprecated DeprecatedFieldsMap) (*yaml.Node, error) {
// Dereference pointers
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"}, nil
}
return buildNode(v.Elem(), comm)
var defaultsElem reflect.Value
if defaults.IsValid() && !defaults.IsNil() {
defaultsElem = defaults.Elem()
}
return buildNodeWithDefaults(v.Elem(), comm, defaultsElem, secrets, deprecated)
}
switch v.Kind() {
case reflect.String:
value := v.String()
return &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: v.String(),
Style: yaml.DoubleQuotedStyle,
Value: value,
Style: getStringStyle(value),
}, nil
case reflect.Struct:
rt := v.Type()
@@ -104,6 +162,26 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) {
continue
}
// Skip deprecated fields if filtering is enabled
if len(deprecated) > 0 && deprecated[typeName][sf.Name] {
continue
}
currentField := v.Field(i)
// If we have defaults, compare and skip if values match
if defaults.IsValid() && i < defaults.NumField() {
defaultField := defaults.Field(i)
currentValue := currentField.Interface()
defaultValue := defaultField.Interface()
isEqual := reflect.DeepEqual(currentValue, defaultValue)
if isEqual {
continue // Skip this field as it matches the default
}
}
// determine key: yaml tag > json tag > field name
yamlTag := sf.Tag.Get("yaml")
jsonTag := sf.Tag.Get("json")
@@ -118,22 +196,43 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) {
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: keyName}
// attach validate and comments inline
// attach validate and comments inline (only when comments are enabled)
var parts []string
if cm := comm[typeName][sf.Name]; cm != "" {
parts = append(parts, cm)
}
if vt := sf.Tag.Get("validate"); vt != "" {
parts = append(parts, fmt.Sprintf(" validate:%s", vt))
// Only add validation tags if comments map is not empty (meaning comments are enabled)
if len(comm) > 0 {
if vt := sf.Tag.Get("validate"); vt != "" {
parts = append(parts, fmt.Sprintf(" validate:%s", vt))
}
}
if len(parts) > 0 {
keyNode.LineComment = strings.Join(parts, " ")
}
valNode, err := buildNode(v.Field(i), comm)
if err != nil {
return nil, err
// Check if this field should be redacted as a secret
var valNode *yaml.Node
var err error
if secrets[typeName][sf.Name] {
valNode = &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: "**hidden**",
Style: yaml.DoubleQuotedStyle, // Keep secrets quoted for clarity
}
} else {
// Pass through the corresponding default field for recursive comparison
var defaultField reflect.Value
if defaults.IsValid() && i < defaults.NumField() {
defaultField = defaults.Field(i)
}
valNode, err = buildNodeWithDefaults(currentField, comm, defaultField, secrets, deprecated)
if err != nil {
return nil, err
}
}
mapNode.Content = append(mapNode.Content, keyNode, valNode)
}
@@ -148,7 +247,11 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) {
// placeholder for empty slice of structs
if v.Len() == 0 && v.Type().Elem().Kind() == reflect.Struct {
zero := reflect.Zero(v.Type().Elem())
n, err := buildNode(zero, comm)
var defaultElem reflect.Value
if defaults.IsValid() && defaults.Len() > 0 {
defaultElem = defaults.Index(0)
}
n, err := buildNodeWithDefaults(zero, comm, defaultElem, secrets, deprecated)
if err != nil {
return nil, err
}
@@ -156,7 +259,11 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) {
return seq, nil
}
for i := 0; i < v.Len(); i++ {
n, err := buildNode(v.Index(i), comm)
var defaultElem reflect.Value
if defaults.IsValid() && i < defaults.Len() {
defaultElem = defaults.Index(i)
}
n, err := buildNodeWithDefaults(v.Index(i), comm, defaultElem, secrets, deprecated)
if err != nil {
return nil, err
}
@@ -164,6 +271,33 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) {
}
return seq, nil
case reflect.Map:
mapNode := &yaml.Node{Kind: yaml.MappingNode}
for _, key := range v.MapKeys() {
// Handle key
keyStr := fmt.Sprintf("%v", key.Interface())
keyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: keyStr,
}
// Handle value recursively
mapVal := v.MapIndex(key)
var defaultVal reflect.Value
if defaults.IsValid() {
defaultVal = defaults.MapIndex(key)
}
valNode, err := buildNodeWithDefaults(mapVal, comm, defaultVal, secrets, deprecated)
if err != nil {
return nil, err
}
mapNode.Content = append(mapNode.Content, keyNode, valNode)
}
return mapNode, nil
default:
n := &yaml.Node{}
if err := n.Encode(v.Interface()); err != nil {
@@ -186,21 +320,73 @@ func GenerateYaml() {
setupSources(true)
setupUrls()
setupFrontend(true)
input := "common/settings/settings.go" // "path to Go source file or directory containing structs"
output := "generated.yaml" // "output YAML file"
output := "generated.yaml" // "output YAML file"
comm, err := collectComments(input)
// Generate YAML with comments enabled, full config, and deprecated fields filtered
// Force the source path to be correct for static generation
yamlContent, err := GenerateConfigYamlWithSource(&Config, true, true, true, "common/settings")
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing comments: %v\n", err)
fmt.Fprintf(os.Stderr, "error generating YAML: %v\n", err)
os.Exit(1)
}
node, err := buildNode(reflect.ValueOf(Config), comm)
if err != nil {
fmt.Fprintf(os.Stderr, "error building YAML node: %v\n", err)
if err := os.WriteFile(output, []byte(yamlContent), 0644); err != nil {
fmt.Fprintf(os.Stderr, "error writing YAML: %v\n", err)
os.Exit(1)
}
fmt.Printf("Generated YAML with comments (deprecated fields filtered): %s\n", output)
}
// GenerateConfigYaml generates YAML from a given config with options for comments and filtering
func GenerateConfigYaml(config *Settings, showComments bool, showFull bool, filterDeprecated bool) (string, error) {
// Try different source paths to handle both runtime and test scenarios
sourcePaths := []string{
"common/settings", // When running from backend directory
".", // When running tests from settings directory
"../settings", // Alternative test path
}
for _, sourcePath := range sourcePaths {
yamlOutput, err := GenerateConfigYamlWithSource(config, showComments, showFull, filterDeprecated, sourcePath)
if err == nil {
return yamlOutput, nil
}
// If it's not a file not found error, return immediately
if !strings.Contains(err.Error(), "no such file or directory") && !strings.Contains(err.Error(), "cannot find") {
return "", err
}
}
// If all paths failed, try with empty maps to at least generate basic YAML
return GenerateConfigYamlWithEmptyMaps(config, showFull)
}
// GenerateConfigYamlWithEmptyMaps generates YAML without comment parsing when source files are unavailable
func GenerateConfigYamlWithEmptyMaps(config *Settings, showFull bool) (string, error) {
// Create empty maps
comm := make(CommentsMap)
secrets := make(SecretFieldsMap)
deprecated := make(DeprecatedFieldsMap)
var node *yaml.Node
var err error
if showFull {
// Show the full current config
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.Value{}, secrets, deprecated)
} else {
// Show only non-default values by comparing with defaults during node building
defaultConfig := setDefaults(true)
defaultConfig.Server.Sources = []Source{{Path: "."}}
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.ValueOf(&defaultConfig), secrets, deprecated)
}
if err != nil {
return "", err
}
// Convert to YAML
doc := &yaml.Node{Kind: yaml.DocumentNode}
doc.Content = []*yaml.Node{node}
@@ -208,17 +394,78 @@ func GenerateYaml() {
enc := yaml.NewEncoder(&rawBuf)
enc.SetIndent(2)
if err := enc.Encode(doc); err != nil {
fmt.Fprintf(os.Stderr, "error encoding YAML: %v\n", err)
os.Exit(1)
return "", err
}
aligned := alignComments(rawBuf.String())
return AlignComments(rawBuf.String()), nil
}
if err := os.WriteFile(output, []byte(aligned), 0644); err != nil {
fmt.Fprintf(os.Stderr, "error writing YAML: %v\n", err)
os.Exit(1)
// GenerateConfigYamlWithSource generates YAML from a given config with options for comments and filtering, using a custom source path
func GenerateConfigYamlWithSource(config *Settings, showComments bool, showFull bool, filterDeprecated bool, sourcePath string) (string, error) {
var comm CommentsMap
var secrets SecretFieldsMap
var deprecated DeprecatedFieldsMap
var err error
if showComments {
// Collect comments, secrets, and deprecated fields from the settings source files
comm, secrets, deprecated, err = CollectCommentsAndSecrets(sourcePath)
if err != nil {
return "", err
}
} else {
// Still need to collect secrets and deprecated fields even if not showing comments
_, secrets, deprecated, err = CollectCommentsAndSecrets(sourcePath)
if err != nil {
return "", err
}
// Create empty comments map
comm = make(CommentsMap)
}
fmt.Printf("Generated YAML with comments: %s\n", output)
// If not filtering deprecated fields, clear the deprecated map
if !filterDeprecated {
deprecated = make(DeprecatedFieldsMap)
}
var node *yaml.Node
if showFull {
// Show the full current config
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.Value{}, secrets, deprecated)
} else {
// Show only non-default values by comparing with defaults during node building
// Create a clean default config (no file loading, just pure defaults)
defaultConfig := setDefaults(true)
// Apply same setup as a fresh instance would have
defaultConfig.Server.Sources = []Source{{Path: "."}}
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.ValueOf(&defaultConfig), secrets, deprecated)
}
if err != nil {
return "", err
}
// Create document
doc := &yaml.Node{Kind: yaml.DocumentNode}
doc.Content = []*yaml.Node{node}
// Encode to YAML
var rawBuf bytes.Buffer
enc := yaml.NewEncoder(&rawBuf)
enc.SetIndent(2)
if err := enc.Encode(doc); err != nil {
return "", err
}
// Apply comment alignment if comments are enabled
yamlOutput := rawBuf.String()
if showComments {
yamlOutput = AlignComments(yamlOutput)
}
return yamlOutput, nil
}
// formatLine applies padding to a single line so its comment starts at a target column.
@@ -257,8 +504,8 @@ func formatLine(line string) string {
return line
}
// alignComments formats each line of the YAML string independently.
func alignComments(input string) string {
// AlignComments formats each line of the YAML string independently.
func AlignComments(input string) string {
lines := strings.Split(input, "\n")
outputLines := make([]string, len(lines))
@@ -0,0 +1,230 @@
package settings
import (
"regexp"
"strings"
"testing"
)
func TestGenerateConfigYaml_Basic(t *testing.T) {
reNumber := regexp.MustCompile(`^-?\d+(\.\d+)?$`)
// Test using the actual source directory structure
config := &Settings{
UserDefaults: UserDefaults{
Locale: "es", // Non-default value
DarkMode: true, // Default value
DisableOfficePreviewExt: ".docx .xlsx", // This field is deprecated
},
Auth: Auth{
Key: "secret123", // This is a secret field
AdminUsername: "testadmin", // This is a secret field
AdminPassword: "testpassword", // This is a secret field
},
}
// Test different combinations
testCases := []struct {
name string
showComments bool
showFull bool
filterDeprecated bool
expectSecrets bool
expectDeprecated bool
}{
{
name: "API_mode_all_fields",
showComments: false,
showFull: true,
filterDeprecated: false,
expectSecrets: true, // Secrets should be hidden
expectDeprecated: true, // Deprecated should be shown
},
{
name: "Static_config_filter_deprecated",
showComments: true,
showFull: true,
filterDeprecated: true,
expectSecrets: true, // Secrets should be hidden
expectDeprecated: false, // Deprecated should be filtered
},
{
name: "Filtered_non_defaults",
showComments: false,
showFull: false,
filterDeprecated: false,
expectSecrets: true, // Secrets should be hidden
expectDeprecated: false, // Might not appear if matches default
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Use the current directory structure for real testing
yamlOutput, err := GenerateConfigYaml(config, tc.showComments, tc.showFull, tc.filterDeprecated)
if err != nil {
t.Fatalf("GenerateConfigYaml failed: %v", err)
}
// Basic validation - should produce some output
if len(yamlOutput) == 0 {
t.Fatal("Generated empty YAML")
}
// Test string quoting - string values should be quoted, numbers and booleans should not
lines := strings.Split(yamlOutput, "\n")
for _, line := range lines {
if strings.Contains(line, ":") && !strings.HasSuffix(strings.TrimSpace(line), ":") {
// This is a key-value line, skip comment portion
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
value := strings.TrimSpace(parts[1])
// Remove comments
if commentIdx := strings.Index(value, "#"); commentIdx >= 0 {
value = strings.TrimSpace(value[:commentIdx])
}
// Skip empty values, arrays, and objects
if value == "" || strings.HasPrefix(value, "[") || strings.HasPrefix(value, "{") {
continue
}
// Check if it's a number (should NOT be quoted)
if reNumber.MatchString(value) {
if strings.HasPrefix(value, "\"") {
t.Errorf("Numeric value should not be quoted in line: %s (value: '%s')", line, value)
}
continue
}
// Check if it's a boolean (should NOT be quoted)
if value == "true" || value == "false" {
if strings.HasPrefix(value, "\"") {
t.Errorf("Boolean value should not be quoted in line: %s (value: '%s')", line, value)
}
continue
}
// Everything else should be a quoted string
if !strings.HasPrefix(value, "\"") || !strings.HasSuffix(value, "\"") {
// Add debug info to see what's wrong
t.Logf("DEBUG: Checking value '%s' from line '%s'", value, strings.TrimSpace(line))
t.Errorf("String value should be quoted in line: %s (value: %s)", strings.TrimSpace(line), value)
}
}
}
}
// Test secret hiding
if tc.expectSecrets {
if !strings.Contains(yamlOutput, "**hidden**") {
t.Error("Expected secrets to be hidden with **hidden**")
}
if strings.Contains(yamlOutput, "secret123") || strings.Contains(yamlOutput, "testadmin") || strings.Contains(yamlOutput, "testpassword") {
t.Error("Secret values should not appear in output")
}
}
// Test deprecated field filtering
hasDeprecated := strings.Contains(yamlOutput, "disableOfficePreviewExt")
if tc.expectDeprecated && !hasDeprecated {
t.Error("Expected deprecated field to be present")
}
if !tc.expectDeprecated && hasDeprecated && tc.filterDeprecated {
t.Error("Expected deprecated field to be filtered out when filterDeprecated=true")
}
// Test comments
hasComments := strings.Contains(yamlOutput, "#")
if tc.showComments && !hasComments {
t.Error("Expected comments to be present when showComments=true")
}
t.Logf("Test case: %s\nGenerated YAML:\n%s\n", tc.name, yamlOutput)
})
}
}
func TestCollectCommentsAndSecrets_Basic(t *testing.T) {
// Test the comment and secret collection directly
comments, secrets, deprecated, err := CollectCommentsAndSecrets(".")
if err != nil {
t.Fatalf("CollectCommentsAndSecrets failed: %v", err)
}
// Should find some secrets in the actual source files
foundSecrets := false
for typeName, typeSecrets := range secrets {
for fieldName := range typeSecrets {
t.Logf("Found secret: %s.%s", typeName, fieldName)
foundSecrets = true
}
}
if !foundSecrets {
t.Error("Expected to find some secret fields in the actual source code")
}
// Should find some deprecated fields
foundDeprecated := false
for typeName, typeDeprecated := range deprecated {
for fieldName := range typeDeprecated {
t.Logf("Found deprecated: %s.%s", typeName, fieldName)
foundDeprecated = true
}
}
if !foundDeprecated {
t.Error("Expected to find some deprecated fields in the actual source code")
}
t.Logf("Comments: %d types", len(comments))
t.Logf("Secrets: %d types", len(secrets))
t.Logf("Deprecated: %d types", len(deprecated))
}
func TestGenerateYaml_StaticGeneration(t *testing.T) {
// Test the static generation function that's used by FILEBROWSER_GENERATE_CONFIG=true
// Create a temporary config for testing
config := &Settings{
UserDefaults: UserDefaults{
Locale: "en",
DisableOfficePreviewExt: ".docx .xlsx", // This should be filtered out
},
Auth: Auth{
Key: "test-secret", // This should be redacted
},
}
// Test static generation with comments and deprecated filtering
yamlOutput, err := GenerateConfigYamlWithSource(config, true, true, true, ".")
if err != nil {
t.Fatalf("Static generation failed: %v", err)
}
// Verify comments are included
if !strings.Contains(yamlOutput, "#") {
t.Error("Static generation should include comments")
}
// Verify deprecated field is filtered out
if strings.Contains(yamlOutput, "disableOfficePreviewExt") {
t.Error("Static generation should filter out deprecated field disableOfficePreviewExt")
}
// Verify secrets are redacted
if !strings.Contains(yamlOutput, "**hidden**") {
t.Error("Static generation should redact secrets")
}
if strings.Contains(yamlOutput, "test-secret") {
t.Error("Static generation should not contain actual secret values")
}
// Verify it has proper structure
if !strings.Contains(yamlOutput, "userDefaults:") {
t.Error("Static generation should contain userDefaults section")
}
if !strings.Contains(yamlOutput, "auth:") {
t.Error("Static generation should contain auth section")
}
t.Logf("Static generation successful with comments and proper filtering")
}
+470
View File
@@ -0,0 +1,470 @@
package settings
import (
"os"
"regexp"
"strings"
"testing"
)
// Test structs for comprehensive testing
type TestConfig struct {
PublicField string `json:"publicField"` // A regular public field
SecretField string `json:"secretField"` // secret: this should be hidden
DeprecatedField string `json:"deprecatedField"` // deprecated: this field is old
EmptyField string `json:"emptyField"` // An empty string field
DefaultField string `json:"defaultField"` // A field that matches default
NonDefaultField string `json:"nonDefaultField"` // A field that differs from default
NestedStruct TestNested `json:"nestedStruct"`
}
type TestNested struct {
NestedPublic string `json:"nestedPublic"` // A nested public field
NestedSecret string `json:"nestedSecret"` // secret: nested secret field
NestedDeprecated string `json:"nestedDeprecated"` // deprecated: nested deprecated field
}
// Helper to create test source files for comment parsing
func createTestSourceFile(t *testing.T, content string) (string, func()) {
tmpFile, err := os.CreateTemp("", "test_*.go")
if err != nil {
t.Fatal(err)
}
if _, err := tmpFile.WriteString(content); err != nil {
t.Fatal(err)
}
tmpFile.Close()
cleanup := func() {
os.Remove(tmpFile.Name())
}
return tmpFile.Name(), cleanup
}
func TestGenerateConfigYaml_StringQuoting(t *testing.T) {
reNumber := regexp.MustCompile(`^-?\d+(\.\d+)?$`)
// Create a simple test source file for this test
sourceContent := `package settings
type Settings struct {
UserDefaults UserDefaults ` + "`json:\"userDefaults\"`" + `
Auth Auth ` + "`json:\"auth\"`" + `
}
type UserDefaults struct {
Locale string ` + "`json:\"locale\"`" + `
}
type Auth struct {
AdminUsername string ` + "`json:\"adminUsername\"`" + `
}
`
tmpFile, cleanup := createTestSourceFile(t, sourceContent)
defer cleanup()
sourcePath := tmpFile[:strings.LastIndex(tmpFile, "/")]
// Create real Settings with string values to test quoting
settings := &Settings{
UserDefaults: UserDefaults{
Locale: "en-US",
},
Auth: Auth{
AdminUsername: "admin",
},
}
// Test with no filtering - should quote all strings
yamlOutput, err := GenerateConfigYamlWithSource(settings, false, true, false, sourcePath)
if err != nil {
t.Fatalf("GenerateConfigYaml failed: %v", err)
}
// All string values should be quoted
lines := strings.Split(yamlOutput, "\n")
for _, line := range lines {
if strings.Contains(line, ":") && !strings.HasSuffix(strings.TrimSpace(line), ":") {
// This is a key-value line
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
value := strings.TrimSpace(parts[1])
// Remove comments
if commentIdx := strings.Index(value, "#"); commentIdx >= 0 {
value = strings.TrimSpace(value[:commentIdx])
}
// Skip empty values, arrays, objects, booleans
if value == "" || value == "false" || value == "true" || strings.HasPrefix(value, "[") || strings.HasPrefix(value, "{") {
continue
}
// Check if it's a number (should NOT be quoted)
if reNumber.MatchString(value) {
if strings.HasPrefix(value, "\"") {
t.Errorf("Numeric value should not be quoted: %s (value: '%s')", line, value)
}
continue
}
// Everything else should be a quoted string
if !strings.HasPrefix(value, "\"") || !strings.HasSuffix(value, "\"") {
t.Errorf("String value should be quoted: %s (value: '%s')", line, value)
}
}
}
}
}
func TestGenerateConfigYaml_SecretHiding(t *testing.T) {
// Create temporary source file with our test structs
sourceContent := `package settings
type TestConfig struct {
PublicField string ` + "`json:\"publicField\"`" + ` // A regular public field
SecretField string ` + "`json:\"secretField\"`" + ` // secret: this should be hidden
}
type TestNested struct {
NestedPublic string ` + "`json:\"nestedPublic\"`" + ` // A nested public field
NestedSecret string ` + "`json:\"nestedSecret\"`" + ` // secret: nested secret field
}
`
tmpFile, cleanup := createTestSourceFile(t, sourceContent)
defer cleanup()
// Test secret detection
_, secrets, _, err := CollectCommentsAndSecrets(tmpFile)
if err != nil {
t.Fatalf("CollectCommentsAndSecrets failed: %v", err)
}
// Verify secret fields are detected
if !secrets["TestConfig"]["SecretField"] {
t.Error("SecretField should be marked as secret")
}
if !secrets["TestNested"]["NestedSecret"] {
t.Error("NestedSecret should be marked as secret")
}
if secrets["TestConfig"]["PublicField"] {
t.Error("PublicField should not be marked as secret")
}
// Test that secrets are properly collected
if len(secrets["TestConfig"]) != 1 {
t.Errorf("Expected 1 secret field in TestConfig, got %d", len(secrets["TestConfig"]))
}
}
func TestGenerateConfigYaml_DeprecatedFiltering(t *testing.T) {
// Create temporary source file with deprecated fields
sourceContent := `package settings
type TestConfig struct {
PublicField string ` + "`json:\"publicField\"`" + ` // A regular public field
DeprecatedField string ` + "`json:\"deprecatedField\"`" + ` // deprecated: this field is old
}
`
tmpFile, cleanup := createTestSourceFile(t, sourceContent)
defer cleanup()
// Test deprecated detection
_, _, deprecated, err := CollectCommentsAndSecrets(tmpFile)
if err != nil {
t.Fatalf("CollectCommentsAndSecrets failed: %v", err)
}
// Verify deprecated fields are detected
if !deprecated["TestConfig"]["DeprecatedField"] {
t.Error("DeprecatedField should be marked as deprecated")
}
if deprecated["TestConfig"]["PublicField"] {
t.Error("PublicField should not be marked as deprecated")
}
}
func TestGenerateConfigYaml_FullVsFiltered(t *testing.T) {
// Create minimal test source
sourceContent := `package settings
type Settings struct {
UserDefaults UserDefaults ` + "`json:\"userDefaults\"`" + `
}
type UserDefaults struct {
DarkMode bool ` + "`json:\"darkMode\"`" + `
Locale string ` + "`json:\"locale\"`" + `
}
`
tmpFile, cleanup := createTestSourceFile(t, sourceContent)
defer cleanup()
sourcePath := tmpFile[:strings.LastIndex(tmpFile, "/")]
// Create a config with both default and non-default values
config := &Settings{
UserDefaults: UserDefaults{
DarkMode: true, // This matches default
Locale: "es", // This differs from default ("en")
},
}
// Test full=true - should show all fields
fullYaml, err := GenerateConfigYamlWithSource(config, false, true, false, sourcePath)
if err != nil {
t.Fatalf("GenerateConfigYaml with full=true failed: %v", err)
}
// Test full=false - should only show non-default fields
filteredYaml, err := GenerateConfigYamlWithSource(config, false, false, false, sourcePath)
if err != nil {
t.Fatalf("GenerateConfigYaml with full=false failed: %v", err)
}
// Full output should be longer than filtered
if len(fullYaml) <= len(filteredYaml) {
t.Error("Full YAML should be longer than filtered YAML")
}
// Filtered output should contain the non-default field
if !strings.Contains(filteredYaml, "locale") {
t.Error("Filtered YAML should contain non-default locale field")
}
// Full output should contain default fields
if !strings.Contains(fullYaml, "darkMode") {
t.Error("Full YAML should contain darkMode field")
}
}
func TestGenerateConfigYaml_CommentsOnOff(t *testing.T) {
// Use the actual settings source directory since comments work there
config := &Settings{
UserDefaults: UserDefaults{
Locale: "en",
},
}
// Test with comments=true using the real source
withComments, err := GenerateConfigYaml(config, true, true, false)
if err != nil {
t.Fatalf("GenerateConfigYaml with comments=true failed: %v", err)
}
// Test with comments=false using the real source
withoutComments, err := GenerateConfigYaml(config, false, true, false)
if err != nil {
t.Fatalf("GenerateConfigYaml with comments=false failed: %v", err)
}
// Output with comments should be longer
if len(withComments) <= len(withoutComments) {
t.Errorf("YAML with comments (%d chars) should be longer than without comments (%d chars)", len(withComments), len(withoutComments))
}
// With comments should contain '#' characters
if !strings.Contains(withComments, "#") {
t.Error("YAML with comments should contain '#' characters")
}
// Without comments should not contain '#' characters
if strings.Contains(withoutComments, "#") {
t.Error("YAML without comments should not contain '#' characters")
}
}
func TestGenerateConfigYaml_IntegrationTest(t *testing.T) {
reNumber := regexp.MustCompile(`^-?\d+(\.\d+)?$`)
// Comprehensive test with all features
config := &Settings{
UserDefaults: UserDefaults{
Locale: "es", // Non-default
DarkMode: true, // Default
DisableOfficePreviewExt: ".docx .xlsx", // This is deprecated
},
Auth: Auth{
Key: "secret123", // This is secret
AdminUsername: "admin", // This is secret
AdminPassword: "password", // This is secret
},
}
tests := []struct {
name string
showComments bool
showFull bool
filterDeprecated bool
expectSecret bool
expectDeprecated bool
expectFull bool
expectComments bool
}{
{
name: "API mode: show all including deprecated",
showComments: false,
showFull: true,
filterDeprecated: false,
expectSecret: true, // secrets should be hidden
expectDeprecated: true, // deprecated should be shown
expectFull: true, // all fields shown
expectComments: false, // no comments
},
{
name: "Static config mode: filter deprecated",
showComments: true,
showFull: true,
filterDeprecated: true,
expectSecret: true, // secrets should be hidden
expectDeprecated: false, // deprecated should be filtered
expectFull: true, // all non-deprecated fields shown
expectComments: true, // comments included
},
{
name: "Filtered output: only changes",
showComments: false,
showFull: false,
filterDeprecated: false,
expectSecret: true, // secrets should be hidden
expectDeprecated: false, // might not appear if matches default
expectFull: false, // only non-default fields
expectComments: false, // no comments
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
yamlOutput, err := GenerateConfigYaml(config, tt.showComments, tt.showFull, tt.filterDeprecated)
if err != nil {
t.Fatalf("GenerateConfigYaml failed: %v", err)
}
// Check secret hiding
if tt.expectSecret {
if !strings.Contains(yamlOutput, "**hidden**") {
t.Error("Expected secrets to be hidden with **hidden**")
}
if strings.Contains(yamlOutput, "secret123") {
t.Error("Secret values should not appear in output")
}
}
// Check deprecated field filtering
hasDeprecated := strings.Contains(yamlOutput, "disableOfficePreviewExt")
if tt.expectDeprecated && !hasDeprecated {
t.Error("Expected deprecated field to be present")
}
if !tt.expectDeprecated && hasDeprecated && tt.filterDeprecated {
t.Error("Expected deprecated field to be filtered out")
}
// Check comments
hasComments := strings.Contains(yamlOutput, "#")
if tt.expectComments && !hasComments {
t.Error("Expected comments to be present")
}
if !tt.expectComments && hasComments {
t.Error("Expected no comments")
}
// Check string quoting - all string values should be quoted
lines := strings.Split(yamlOutput, "\n")
for _, line := range lines {
if strings.Contains(line, ":") && !strings.HasSuffix(strings.TrimSpace(line), ":") {
// This is a key-value line
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
value := strings.TrimSpace(parts[1])
// Skip comments and empty values
if commentIdx := strings.Index(value, "#"); commentIdx >= 0 {
// If comment starts at the beginning, there's no value, just a comment
if commentIdx == 0 {
continue
}
value = strings.TrimSpace(value[:commentIdx])
}
// Check if it's a number (should NOT be quoted)
if reNumber.MatchString(value) {
if strings.HasPrefix(value, "\"") {
t.Errorf("Numeric value should not be quoted in line: %s (value: '%s')", line, value)
}
continue
}
// Check if it's a boolean (should NOT be quoted)
if value == "true" || value == "false" {
if strings.HasPrefix(value, "\"") {
t.Errorf("Boolean value should not be quoted in line: %s (value: '%s')", line, value)
}
continue
}
// Skip arrays, objects, and empty values
if value == "" || strings.HasPrefix(value, "[") || strings.HasPrefix(value, "{") || strings.Contains(value, ":") {
continue
}
// Everything else should be a quoted string
if !strings.HasPrefix(value, "\"") || !strings.HasSuffix(value, "\"") {
t.Errorf("String value should be quoted in line: %s (value: '%s')", line, value)
}
}
}
}
t.Logf("Generated YAML for %s:\n%s", tt.name, yamlOutput)
})
}
}
func TestGenerateConfigYaml_EdgeCases(t *testing.T) {
tests := []struct {
name string
config *Settings
desc string
}{
{
name: "empty_config",
config: &Settings{},
desc: "Empty config should generate valid YAML",
},
{
name: "only_defaults",
config: &Settings{
UserDefaults: UserDefaults{
DarkMode: true, // default value
Locale: "en", // default value
},
},
desc: "Config with only default values",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test all combinations
for _, showComments := range []bool{false, true} {
for _, showFull := range []bool{false, true} {
for _, filterDeprecated := range []bool{false, true} {
yamlOutput, err := GenerateConfigYaml(tt.config, showComments, showFull, filterDeprecated)
if err != nil {
t.Fatalf("GenerateConfigYaml failed for %s (comments=%v, full=%v, filterDeprecated=%v): %v",
tt.desc, showComments, showFull, filterDeprecated, err)
}
// Basic validation - should produce valid YAML structure
if yamlOutput == "" {
t.Errorf("Generated empty YAML for %s", tt.desc)
}
// Should not contain obvious error messages (but allow "error" in log levels etc)
if strings.Contains(yamlOutput, "Error:") || strings.Contains(yamlOutput, "ERROR:") {
t.Errorf("YAML contains error for %s", tt.desc)
}
}
}
}
})
}
}
+31 -23
View File
@@ -21,6 +21,8 @@ type Settings struct {
}
type Server struct {
MinSearchLength int `json:"minSearchLength"` // minimum length of search query to begin searching (default: 3)
DisableUpdateCheck bool `json:"disableUpdateCheck"` // disables backend update check service
NumImageProcessors int `json:"numImageProcessors"` // number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
Socket string `json:"socket"` // socket to listen on
TLSKey string `json:"tlsKey"` // path to TLS key
@@ -52,9 +54,9 @@ type Integrations struct {
// onlyoffice secret is stored in the local.json file
// docker exec <containerID> /var/www/onlyoffice/documentserver/npm/json -f /etc/onlyoffice/documentserver/local.json 'services.CoAuthoring.secret.session.string'
type OnlyOffice struct {
Url string `json:"url" validate:"required"` // The URL to the OnlyOffice Document Server, needs to be accessible to the user.
InternalUrl string `json:"internalUrl"` // An optional internal address that the filebrowser server can use to communicate with the OnlyOffice Document Server, could be useful to bypass proxy.
Secret string `json:"secret" validate:"required"`
Url string `json:"url" validate:"required"` // The URL to the OnlyOffice Document Server, needs to be accessible to the user.
InternalUrl string `json:"internalUrl"` // An optional internal address that the filebrowser server can use to communicate with the OnlyOffice Document Server, could be useful to bypass proxy.
Secret string `json:"secret" validate:"required"` // secret: authentication key for OnlyOffice integration
}
type Media struct {
@@ -73,22 +75,22 @@ type LogConfig struct {
type Source struct {
Path string `json:"path" validate:"required"` // file system path. (Can be relative)
Name string `json:"name"` // display name
Config SourceConfig `json:"config"`
Config SourceConfig `json:"config,omitempty"`
}
type SourceConfig struct {
DenyByDefault bool `json:"denyByDefault"` // deny access unless an "allow" access rule was specifically created.
Private bool `json:"private"` // designate as source as private -- currently just means no sharing permitted.
Disabled bool `json:"disabled"` // disable the source, this is useful so you don't need to remove it from the config file
IndexingInterval uint32 `json:"indexingIntervalMinutes"` // optional manual overide interval in minutes to re-index the source
DisableIndexing bool `json:"disableIndexing"` // disable the indexing of this source
MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported
NeverWatchPaths []string `json:"neverWatchPaths"` // paths that get initially once. Useful for folders that rarely change contents (without source path prefix)
Exclude ExcludeIndexFilter `json:"exclude"` // exclude files and folders from indexing, if include is not set
Include IncludeIndexFilter `json:"include"` // include files and folders from indexing, if exclude is not set
DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path
DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users?
CreateUserDir bool `json:"createUserDir"` // create a user directory for each user
DenyByDefault bool `json:"denyByDefault,omitempty"` // deny access unless an "allow" access rule was specifically created.
Private bool `json:"private"` // designate as source as private -- currently just means no sharing permitted.
Disabled bool `json:"disabled,omitempty"` // disable the source, this is useful so you don't need to remove it from the config file
IndexingInterval uint32 `json:"indexingIntervalMinutes,omitempty"` // optional manual overide interval in minutes to re-index the source
DisableIndexing bool `json:"disableIndexing,omitempty"` // disable the indexing of this source
MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported
NeverWatchPaths []string `json:"neverWatchPaths"` // paths that get initially once. Useful for folders that rarely change contents (without source path prefix)
Exclude ExcludeIndexFilter `json:"exclude"` // exclude files and folders from indexing, if include is not set
Include IncludeIndexFilter `json:"include"` // include files and folders from indexing, if exclude is not set
DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path
DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users?
CreateUserDir bool `json:"createUserDir"` // create a user directory for each user
}
type IncludeIndexFilter struct {
@@ -114,19 +116,24 @@ type Frontend struct {
ExternalLinks []ExternalLink `json:"externalLinks"`
DisableNavButtons bool `json:"disableNavButtons"` // disable the nav buttons in the sidebar
Styling StylingConfig `json:"styling"`
Favicon string `json:"favicon"` // path to a favicon to use for the frontend
Description string `json:"description"` // description that shows up in html head meta description
}
type StylingConfig struct {
CustomCSS string `json:"customCSS"` // if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css")
LightBackground string `json:"lightBackground"` // specify a valid CSS color property value to use as the background color in light mode
DarkBackground string `json:"darkBackground"` // Specify a valid CSS color property value to use as the background color in dark mode
CustomThemes map[string]CustomTheme `json:"customThemes"` // A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option.
CustomThemeOptions map[string]CustomTheme `json:"-"` // not exposed
CustomCSS string `json:"customCSS"` // if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css")
CustomCSSRaw string `json:"-"` // The css raw content to use for the custom css.
LightBackground string `json:"lightBackground"` // specify a valid CSS color property value to use as the background color in light mode
DarkBackground string `json:"darkBackground"` // Specify a valid CSS color property value to use as the background color in dark mode
CustomThemes map[string]CustomTheme `json:"customThemes"` // A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option.
// In-memory (not exposed to config)
CustomThemeOptions map[string]CustomTheme `json:"-"` // not exposed
}
type CustomTheme struct {
Description string `json:"description"` // The description of the theme to display in the UI.
CSS string `json:"css,omitempty"` // The css file path and filename to use for the theme.
CssRaw string `json:"-"` // The css raw content to use for the theme.
}
type ExternalLink struct {
@@ -152,8 +159,8 @@ type UserDefaults struct {
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
DisablePreviewExt string `json:"disablePreviewExt"` // comma separated list of file extensions to disable preview for
DisableViewingExt string `json:"disableViewingExt"` // comma separated list of file extensions to disable viewing for
DisablePreviewExt string `json:"disablePreviewExt"` // space separated list of file extensions to disable preview for
DisableViewingExt string `json:"disableViewingExt"` // space separated list of file extensions to disable viewing for
LockPassword bool `json:"lockPassword"` // disable the user from changing their password
DisableSettings bool `json:"disableSettings,omitempty"` // disable the user from viewing the settings page
Preview users.Preview `json:"preview"`
@@ -167,4 +174,5 @@ type UserDefaults struct {
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // list of file extensions to disable onlyoffice editor for
CustomTheme string `json:"customTheme"` // Name of theme to use chosen from custom themes config.
ShowSelectMultiple bool `json:"showSelectMultiple"` // show select multiple files on desktop
DebugOffice bool `json:"debugOffice"` // debug onlyoffice editor
}
+8 -4
View File
@@ -74,17 +74,21 @@ func FallbackColor(val, defaultColor string) string {
}
func addCustomTheme(name, description, cssFilePath string) {
// Store only file path in config (for YAML export)
Config.Frontend.Styling.CustomThemes[name] = CustomTheme{
Description: description,
CSS: cssFilePath, // Store file path, not content
}
// Load CSS content for runtime use (frontend consumption)
cssContent := ""
if cssFilePath != "" {
cssContent = readCustomCSS(cssFilePath)
}
Config.Frontend.Styling.CustomThemes[name] = CustomTheme{
Description: description,
CSS: cssContent,
}
Config.Frontend.Styling.CustomThemeOptions[name] = CustomTheme{
Description: description,
CSS: cssFilePath,
CssRaw: cssContent, // Store loaded content
}
}
+1
View File
@@ -12,4 +12,5 @@ var (
SearchResultsCache = cache.NewCache(15*time.Second, 1*time.Hour)
OnlyOfficeCache = cache.NewCache(48*time.Hour, 1*time.Hour)
JwtCache = cache.NewCache(1*time.Hour, 72*time.Hour)
MediaCache = cache.NewCache(24 * time.Hour) // subtitles get cached for 24 hours
)
+7
View File
@@ -121,3 +121,10 @@ func NonNilSlice[T any](in []T) []T {
}
return in
}
func Ternary[T any](cond bool, vtrue, vfalse T) T {
if cond {
return vtrue
}
return vfalse
}
+76 -13
View File
@@ -176,6 +176,7 @@ func (s *Storage) DenyUser(sourcePath, indexPath, username string) error {
}
rule.Deny.Users[username] = struct{}{}
s.incrementSourceVersion(sourcePath)
accessCache.Set(accessChangedKey+sourcePath, false)
return s.SaveToDB()
}
@@ -195,6 +196,7 @@ func (s *Storage) AllowUser(sourcePath, indexPath, username string) error {
}
rule.Allow.Users[username] = struct{}{}
s.incrementSourceVersion(sourcePath)
accessCache.Set(accessChangedKey+sourcePath, false)
return s.SaveToDB()
}
@@ -212,6 +214,7 @@ func (s *Storage) DenyGroup(sourcePath, indexPath, groupname string) error {
}
rule.Deny.Groups[groupname] = struct{}{}
s.incrementSourceVersion(sourcePath)
accessCache.Set(accessChangedKey+sourcePath, false)
return s.SaveToDB()
}
@@ -229,6 +232,7 @@ func (s *Storage) AllowGroup(sourcePath, indexPath, groupname string) error {
}
rule.Allow.Groups[groupname] = struct{}{}
s.incrementSourceVersion(sourcePath)
accessCache.Set(accessChangedKey+sourcePath, false)
return s.SaveToDB()
}
@@ -242,6 +246,7 @@ func (s *Storage) DenyAll(sourcePath, indexPath string) error {
}
rule.DenyAll = true
s.incrementSourceVersion(sourcePath)
accessCache.Set(accessChangedKey+sourcePath, false)
return s.SaveToDB()
}
@@ -268,32 +273,91 @@ func (s *Storage) Permitted(sourcePath, indexPath, username string) bool {
}
func (s *Storage) computePermitted(sourcePath, indexPath, username string) bool {
var rulesFound []*AccessRule
// Walk up the path hierarchy and collect all relevant rules
currentPath := indexPath
for {
permitted, found := s.permittedAtExactPath(sourcePath, indexPath, username)
rule, found := s.getRuleAtExactPath(sourcePath, currentPath)
if found {
return permitted
rulesFound = append(rulesFound, rule)
}
indexPath = utils.GetParentDirectoryPath(indexPath)
if indexPath == "" {
if currentPath == "/" || currentPath == "." || currentPath == "" {
break
}
currentPath = utils.GetParentDirectoryPath(currentPath)
}
// Now evaluate the rules, starting from the most specific (indexPath) to the least specific (root)
for _, rule := range rulesFound {
permitted, hasSpecificRule := s.evaluateRuleForUser(rule, username)
if hasSpecificRule {
return permitted
}
}
// No rules found anywhere in the path hierarchy
// Check if the source has DenyByDefault configured (acts like a root-level denyAll rule)
// No specific user or group rule found in the hierarchy.
// Check for any DenyAll rule in the path.
for _, rule := range rulesFound {
if rule.DenyAll {
return false
}
}
// No specific rules found anywhere in the path hierarchy.
// Fallback to the source's DenyByDefault setting.
sourceInfo, ok := settings.Config.Server.SourceMap[sourcePath]
if !ok {
logger.Errorf("source %s not found in config during access check", sourcePath)
return false
}
if sourceInfo.Config.DenyByDefault {
return false
}
return true
return !sourceInfo.Config.DenyByDefault
}
// getRuleAtExactPath is a helper to get a rule without the recursive logic.
func (s *Storage) getRuleAtExactPath(sourcePath, indexPath string) (*AccessRule, bool) {
s.mux.RLock()
defer s.mux.RUnlock()
rulesBySource, ok := s.AllRules[sourcePath]
if !ok {
return nil, false
}
rule, ok := rulesBySource[indexPath]
return rule, ok
}
// evaluateRuleForUser evaluates a single rule for a user and returns if a specific rule was found.
func (s *Storage) evaluateRuleForUser(rule *AccessRule, username string) (permitted bool, hasSpecificRule bool) {
// Check user deny first
if _, found := rule.Deny.Users[username]; found {
return false, true
}
// Check group deny
for group := range rule.Deny.Groups {
if s.isUserInGroup(username, group) {
return false, true
}
}
// Check user allow
if _, found := rule.Allow.Users[username]; found {
return true, true
}
// Check group allow
for group := range rule.Allow.Groups {
if s.isUserInGroup(username, group) {
return true, true
}
}
// No specific rule for this user in this rule set.
return false, false
}
/*
// permittedAtExactPath checks if a rule exists at the given path and evaluates it if so.
func (s *Storage) permittedAtExactPath(sourcePath, indexPath, username string) (bool, bool) {
s.mux.RLock()
@@ -366,6 +430,7 @@ func (s *Storage) permittedRule(rule *AccessRule, username string, sourcePath st
return true
}
*/
// isUserInGroup checks if a username is in a group.
func (s *Storage) isUserInGroup(username, group string) bool {
@@ -416,8 +481,6 @@ func (s *Storage) GetFrontendRules(sourcePath, indexPath string) (FrontendAccess
frontendRules.Deny.Groups = utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups)))
frontendRules.Allow.Users = utils.NonNilSlice(slices.Collect(maps.Keys(rule.Allow.Users)))
frontendRules.Allow.Groups = utils.NonNilSlice(slices.Collect(maps.Keys(rule.Allow.Groups)))
logger.Debugf("Returning frontend rules for source: %s and index: %s", sourcePath, indexPath)
return frontendRules, ok
}
+74
View File
@@ -405,3 +405,77 @@ func TestPermitted_DenyByDefaultWithDenyAll(t *testing.T) {
t.Error("bob should not be permitted (DenyByDefault is true and no allow rule)")
}
}
func TestPermitted_DenyByDefault_AdminRootAccess(t *testing.T) {
// 1. Setup
access.ClearCache()
s, userStore := createTestStorage(t)
createTestUser(t, userStore, "admin")
createTestUser(t, userStore, "graham")
createTestUser(t, userStore, "bob")
err := s.LoadFromDB()
if err != nil && err != storm.ErrNotFound {
t.Errorf("unexpected error loading from DB: %v", err)
}
// 2. Configure source with DenyByDefault: true
originalSourceMap := settings.Config.Server.SourceMap
defer func() {
settings.Config.Server.SourceMap = originalSourceMap
access.ClearCache()
}()
settings.Config.Server.SourceMap = map[string]settings.Source{
"mnt/secure": {
Path: "mnt/secure",
Name: "secure",
Config: settings.SourceConfig{
DenyByDefault: true,
},
},
}
// 3. Set up access rules
if err := s.AllowUser("mnt/secure", "/", "admin"); err != nil {
t.Fatalf("AllowUser for admin failed: %v", err)
}
if err := s.AllowUser("mnt/secure", "/test", "graham"); err != nil {
t.Fatalf("AllowUser for graham failed: %v", err)
}
// 4. Assertions
// Admin checks
if !s.Permitted("mnt/secure", "/", "admin") {
t.Error("admin should be permitted for /")
}
if !s.Permitted("mnt/secure", "/test", "admin") {
t.Error("admin should be permitted for /test")
}
if !s.Permitted("mnt/secure", "/test/sub", "admin") {
t.Error("admin should be permitted for /test/sub (this is the bug)")
}
// Graham checks
if !s.Permitted("mnt/secure", "/test", "graham") {
t.Error("graham should be permitted for /test")
}
if !s.Permitted("mnt/secure", "/test/sub", "graham") {
t.Error("graham should be permitted for /test/sub")
}
if s.Permitted("mnt/secure", "/", "graham") {
t.Error("graham should NOT be permitted for /")
}
if s.Permitted("mnt/secure", "/anotherfolder", "graham") {
t.Error("graham should NOT be permitted for /anotherfolder")
}
// Bob checks (should have no access)
if s.Permitted("mnt/secure", "/", "bob") {
t.Error("bob should NOT be permitted for /")
}
if s.Permitted("mnt/secure", "/test", "bob") {
t.Error("bob should NOT be permitted for /test")
}
if s.Permitted("mnt/secure", "/test/sub", "bob") {
t.Error("bob should NOT be permitted for /test/sub")
}
}
+25 -22
View File
@@ -4,28 +4,31 @@ import "sync"
type CommonShare struct {
//AllowEdit bool `json:"allowEdit,omitempty"`
AllowUpload bool `json:"allowUpload,omitempty"`
DisableFileViewer bool `json:"disableFileViewer,omitempty"`
DownloadsLimit int `json:"downloadsLimit,omitempty"`
ShareTheme string `json:"shareTheme,omitempty"`
DisableAnonymous bool `json:"disableAnonymous,omitempty"`
MaxBandwidth int `json:"maxBandwidth,omitempty"`
DisableThumbnails bool `json:"disableThumbnails,omitempty"`
KeepAfterExpiration bool `json:"keepAfterExpiration,omitempty"`
AllowedUsernames []string `json:"allowedUsernames,omitempty"`
ThemeColor string `json:"themeColor,omitempty"`
Banner string `json:"banner,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Favicon string `json:"favicon,omitempty"`
QuickDownload bool `json:"quickDownload,omitempty"`
HideNavButtons bool `json:"hideNavButtons,omitempty"`
DisableSidebar bool `json:"disableSidebar"`
ViewMode string `json:"viewMode,omitempty"`
Source string `json:"source,omitempty"` // backend source is path to maintain between name changes
Path string `json:"path,omitempty"`
DownloadURL string `json:"downloadURL,omitempty"`
DisableShareCard bool `json:"disableShareCard,omitempty"`
AllowUpload bool `json:"allowUpload,omitempty"`
DisableFileViewer bool `json:"disableFileViewer,omitempty"`
DownloadsLimit int `json:"downloadsLimit,omitempty"`
ShareTheme string `json:"shareTheme,omitempty"`
DisableAnonymous bool `json:"disableAnonymous,omitempty"`
MaxBandwidth int `json:"maxBandwidth,omitempty"`
DisableThumbnails bool `json:"disableThumbnails,omitempty"`
KeepAfterExpiration bool `json:"keepAfterExpiration,omitempty"`
AllowedUsernames []string `json:"allowedUsernames,omitempty"`
ThemeColor string `json:"themeColor,omitempty"`
Banner string `json:"banner,omitempty"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Favicon string `json:"favicon,omitempty"`
QuickDownload bool `json:"quickDownload,omitempty"`
HideNavButtons bool `json:"hideNavButtons,omitempty"`
DisableSidebar bool `json:"disableSidebar"`
ViewMode string `json:"viewMode,omitempty"`
Source string `json:"source,omitempty"` // backend source is path to maintain between name changes
Path string `json:"path,omitempty"`
DownloadURL string `json:"downloadURL,omitempty"`
DisableShareCard bool `json:"disableShareCard,omitempty"`
EnforceDarkLightMode string `json:"enforceDarkLightMode,omitempty"` // "dark" or "light"
EnableOnlyOffice bool `json:"enableOnlyOffice,omitempty"`
EnableOnlyOfficeEditing bool `json:"enableOnlyOfficeEditing,omitempty"`
}
type CreateBody struct {
CommonShare
+4 -6
View File
@@ -49,6 +49,7 @@ type Preview struct {
Office bool `json:"office"` // show preview image for office files
PopUp bool `json:"popup"` // show larger popup preview when hovering
AutoplayMedia bool `json:"autoplayMedia"` // autoplay media files in preview
DefaultMediaPlayer bool `json:"defaultMediaPlayer"` // disable html5 media player and use the default media player
}
// User describes a user.
@@ -99,10 +100,11 @@ type NonAdminEditable struct {
FileLoading FileLoading `json:"fileLoading"` // upload and download settings
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // deprecated
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // deprecated
DisablePreviewExt string `json:"disablePreviewExt"` // comma separated list of file extensions to disable preview for
DisableViewingExt string `json:"disableViewingExt"` // comma separated list of file extensions to disable viewing for
DisablePreviewExt string `json:"disablePreviewExt"` // space separated list of file extensions to disable preview for
DisableViewingExt string `json:"disableViewingExt"` // space separated list of file extensions to disable viewing for
CustomTheme string `json:"customTheme"` // Name of theme to use chosen from custom themes config.
ShowSelectMultiple bool `json:"showSelectMultiple"` // show select multiple files on desktop
DebugOffice bool `json:"debugOffice"` // debug onlyoffice editor
}
type FileLoading struct {
@@ -110,10 +112,6 @@ type FileLoading struct {
ChunkSize int `json:"uploadChunkSizeMb"`
}
var AnonymousUser = User{
Username: "anonymous", // temp user not registered
}
func CleanUsername(s string) string {
// Remove any trailing space to avoid ending on -
s = strings.Trim(s, " ")
+28 -5
View File
@@ -75,7 +75,12 @@ func Register(username string, sources []string) chan EventMessage {
func Unregister(username string, ch chan EventMessage) {
userClientsMu.Lock()
conns := userClients[username]
defer userClientsMu.Unlock()
conns, ok := userClients[username]
if !ok {
// User already cleaned up by Shutdown, nothing to do.
return
}
for i, c := range conns {
if c == ch {
userClients[username] = append(conns[:i], conns[i+1:]...)
@@ -85,18 +90,15 @@ func Unregister(username string, ch chan EventMessage) {
if len(userClients[username]) == 0 {
delete(userClients, username)
}
userClientsMu.Unlock()
sourceClientsMu.Lock()
defer sourceClientsMu.Unlock()
for source, clients := range sourceClients {
delete(clients, ch)
if len(clients) == 0 {
delete(sourceClients, source)
}
}
sourceClientsMu.Unlock()
close(ch)
}
func SendToUsers(eventType, message string, users []string) {
@@ -138,3 +140,24 @@ func handleSourceUpdates() {
}
}
}
func Shutdown() {
userClientsMu.Lock()
defer userClientsMu.Unlock()
sourceClientsMu.Lock()
defer sourceClientsMu.Unlock()
for username, clientChannels := range userClients {
for _, ch := range clientChannels {
// Clean up source clients
for source, clients := range sourceClients {
delete(clients, ch)
if len(clients) == 0 {
delete(sourceClients, source)
}
}
close(ch)
}
delete(userClients, username)
}
}
+341
View File
@@ -0,0 +1,341 @@
package ffmpeg
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
// ImageService handles image operations with ffmpeg
type ImageService struct {
ffmpegPath string
ffprobePath string
debug bool
}
// NewImageService creates a new image service instance
func NewImageService(ffmpegPath, ffprobePath string, debug bool) *ImageService {
return &ImageService{
ffmpegPath: ffmpegPath,
ffprobePath: ffprobePath,
debug: debug,
}
}
// GetImageDimensions extracts the dimensions of an image file using ffprobe
func (s *ImageService) GetImageDimensions(imagePath string) (width, height int, err error) {
fmt.Printf("🔍 IMAGE SERVICE: Getting dimensions for %s\n", filepath.Base(imagePath))
fmt.Printf("🔧 IMAGE SERVICE: Using ffprobe at: %s\n", s.ffprobePath)
// Get HEIC dimensions (fallback to individual stream dimensions for nowe re
probeCmd := exec.Command(
s.ffprobePath,
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height",
"-of", "csv=p=0:s=x",
imagePath,
)
fmt.Printf("🚀 IMAGE SERVICE: Running command: %s %s\n", s.ffprobePath, strings.Join(probeCmd.Args[1:], " "))
var probeOut bytes.Buffer
var probeErr bytes.Buffer
probeCmd.Stdout = &probeOut
probeCmd.Stderr = &probeErr
if err = probeCmd.Run(); err != nil {
fmt.Printf("❌ IMAGE SERVICE: ffprobe failed with error: %v\n", err)
fmt.Printf("❌ IMAGE SERVICE: ffprobe stderr: %s\n", probeErr.String())
return 0, 0, fmt.Errorf("ffprobe command failed on image file '%s': %w", imagePath, err)
}
dimensions := strings.TrimSpace(probeOut.String())
fmt.Printf("📏 IMAGE SERVICE: Got raw dimensions string: '%s'\n", dimensions)
// Handle cases where ffprobe returns multiple lines or extra characters
// Take the first line and clean it up
lines := strings.Split(dimensions, "\n")
cleanDimensions := strings.TrimSpace(lines[0])
// Remove any trailing 'x' characters that might appear
cleanDimensions = strings.TrimSuffix(cleanDimensions, "x")
fmt.Printf("📏 IMAGE SERVICE: Cleaned dimensions string: '%s'\n", cleanDimensions)
parts := strings.Split(cleanDimensions, "x")
if len(parts) != 2 {
fmt.Printf("❌ IMAGE SERVICE: Invalid dimensions format after cleaning: %s\n", cleanDimensions)
return 0, 0, fmt.Errorf("invalid dimensions format: %s", cleanDimensions)
}
width, err = strconv.Atoi(parts[0])
if err != nil {
fmt.Printf("❌ IMAGE SERVICE: Invalid width: %s\n", parts[0])
return 0, 0, fmt.Errorf("invalid width: %w", err)
}
height, err = strconv.Atoi(parts[1])
if err != nil {
fmt.Printf("❌ IMAGE SERVICE: Invalid height: %s\n", parts[1])
return 0, 0, fmt.Errorf("invalid height: %w", err)
}
fmt.Printf("✅ IMAGE SERVICE: Parsed dimensions: %dx%d\n", width, height)
return width, height, nil
}
// ConvertHEICToJPEG converts a HEIC file to JPEG with specified dimensions and quality using proper tile extraction
func (s *ImageService) ConvertHEICToJPEG(heicPath string, targetWidth, targetHeight int, quality string) ([]byte, error) {
fmt.Printf("🎯 IMAGE SERVICE: Converting HEIC %s to JPEG (%dx%d, quality %s) using proper tile extraction\n", filepath.Base(heicPath), targetWidth, targetHeight, quality)
// Create temporary directory for tile processing
outputDir := filepath.Dir(heicPath)
tempDir := filepath.Join(outputDir, fmt.Sprintf("heic_tiles_%d", os.Getpid()))
err := os.MkdirAll(tempDir, 0755)
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
defer func() {
fmt.Printf("🧹 IMAGE SERVICE: Cleaning up temporary directory: %s\n", tempDir)
os.RemoveAll(tempDir)
}()
// Step 1: Get grid info using trace output
fmt.Printf("📐 IMAGE SERVICE: Step 1 - Getting grid info from HEIC trace\n")
// Use ffprobe with trace level to get grid information
gridCmd := exec.Command(s.ffmpegPath, "-loglevel", "trace", "-i", heicPath, "-f", "null", "-")
var gridOut bytes.Buffer
var gridErr bytes.Buffer
gridCmd.Stdout = &gridOut
gridCmd.Stderr = &gridErr
_ = gridCmd.Run() // Don't check error, we're just extracting info
traceOutput := gridErr.String()
fmt.Printf("📊 IMAGE SERVICE: Got trace output: %d bytes\n", len(traceOutput))
// Parse grid dimensions from trace output
var gridCols, gridRows = 8, 6 // Default fallback
var val int
// Look for grid_row and grid_col in trace output
lines := strings.Split(traceOutput, "\n")
for _, line := range lines {
if strings.Contains(line, "grid_row") {
// Try to extract grid_row value
if parts := strings.Split(line, "grid_row"); len(parts) > 1 {
if matches := strings.Fields(parts[1]); len(matches) > 0 {
if val, err = strconv.Atoi(strings.Trim(matches[0], " :=")); err == nil {
gridRows = val + 1 // HEIC grid_row is 0-based
fmt.Printf("📐 IMAGE SERVICE: Parsed grid_row: %d (rows: %d)\n", val, gridRows)
}
}
}
}
if strings.Contains(line, "grid_col") {
// Try to extract grid_col value
if parts := strings.Split(line, "grid_col"); len(parts) > 1 {
if matches := strings.Fields(parts[1]); len(matches) > 0 {
if val, err = strconv.Atoi(strings.Trim(matches[0], " :=")); err == nil {
gridCols = val + 1 // HEIC grid_col is 0-based
fmt.Printf("📐 IMAGE SERVICE: Parsed grid_col: %d (cols: %d)\n", val, gridCols)
}
}
}
}
}
fmt.Printf("📊 IMAGE SERVICE: Using grid dimensions: %dx%d\n", gridCols, gridRows)
// Use FFmpeg's auto-orientation feature instead of forced rotation
fmt.Printf("🔄 IMAGE SERVICE: Using FFmpeg auto-orientation for proper image orientation\n")
orientationFilter := ",auto-orient=1"
// Step 2: Extract tile streams (skip stream 0 which is compatibility image)
fmt.Printf("🔧 IMAGE SERVICE: Step 2 - Extracting tile streams\n")
// Extract using the exact approach that works: ffmpeg -i input.heic -map 0 output_%d.png
// But we'll skip the first output (stream 0) since it's the compatibility image
tilesPattern := filepath.Join(tempDir, "output_%d.png")
extractCmd := exec.Command(
s.ffmpegPath,
"-i", heicPath,
"-map", "0", // Map all streams
"-y",
tilesPattern,
)
var extractOut bytes.Buffer
var extractErr bytes.Buffer
extractCmd.Stdout = &extractOut
extractCmd.Stderr = &extractErr
fmt.Printf("🚀 IMAGE SERVICE: Running tile extraction: %s %s\n", s.ffmpegPath, strings.Join(extractCmd.Args[1:], " "))
if err = extractCmd.Run(); err != nil {
fmt.Printf("❌ IMAGE SERVICE: Tile extraction failed: %v\n", err)
fmt.Printf("❌ IMAGE SERVICE: stderr: %s\n", extractErr.String())
return nil, fmt.Errorf("tile extraction failed: %w", err)
}
fmt.Printf("✅ IMAGE SERVICE: Tiles extracted successfully\n")
// Step 3: Filter tiles (skip output_0.png which is compatibility image)
fmt.Printf("🔧 IMAGE SERVICE: Step 3 - Filtering tiles\n")
files, err := filepath.Glob(filepath.Join(tempDir, "output_*.png"))
if err != nil || len(files) == 0 {
return nil, fmt.Errorf("no tiles extracted, found %d files", len(files))
}
fmt.Printf("📊 IMAGE SERVICE: Found %d extracted files\n", len(files))
// Create filtered tiles, skipping output_0.png (compatibility image)
// and rename them to sequential tile_001.png, tile_002.png, etc.
tileIndex := 1
expectedTiles := gridCols * gridRows
for i := 1; i <= len(files); i++ { // Start from 1 to skip output_0.png
sourceFile := filepath.Join(tempDir, fmt.Sprintf("output_%d.png", i))
var info os.FileInfo
if info, err = os.Stat(sourceFile); err == nil && info.Size() > 500 {
// This is a valid tile, rename it to sequential format
targetFile := filepath.Join(tempDir, fmt.Sprintf("tile_%03d.png", tileIndex))
if err = os.Rename(sourceFile, targetFile); err != nil {
fmt.Printf("⚠️ IMAGE SERVICE: Failed to rename %s: %v\n", filepath.Base(sourceFile), err)
continue
}
fmt.Printf("✅ IMAGE SERVICE: Valid tile %d: %s (%d bytes)\n", tileIndex, filepath.Base(targetFile), info.Size())
tileIndex++
// Stop if we have enough tiles
if tileIndex > expectedTiles {
break
}
} else {
fmt.Printf("⚠️ IMAGE SERVICE: Skipping invalid tile: output_%d.png\n", i)
}
}
actualTiles := tileIndex - 1
fmt.Printf("📊 IMAGE SERVICE: Using %d valid tiles (expected %d) with grid %dx%d\n", actualTiles, expectedTiles, gridCols, gridRows)
// Step 4: Merge tiles back into final image with proper quality and rotation
outputFile := filepath.Join(tempDir, "final_output.jpg")
// Build the video filter chain
inputPattern := filepath.Join(tempDir, "tile_%03d.png")
// Create filter chain: tile assembly + auto-orientation + proper scaling
var filterChain string
// For original size (targetWidth=0, targetHeight=0), don't scale
if targetWidth == 0 && targetHeight == 0 {
// This is original size - no scaling, maximum quality
// Use padding=0:margin=0 to eliminate black borders between tiles
filterChain = fmt.Sprintf("tile=%dx%d:nb_frames=%d:padding=0:margin=0%s",
gridCols, gridRows, actualTiles, orientationFilter)
fmt.Printf("🎯 IMAGE SERVICE: Using ORIGINAL size - no scaling, auto-orientation applied\n")
} else {
// This is a resize request - apply scaling with proper aspect handling
filterChain = fmt.Sprintf("tile=%dx%d:nb_frames=%d:padding=0:margin=0%s,scale=%d:%d:force_original_aspect_ratio=decrease:eval=frame",
gridCols, gridRows, actualTiles, orientationFilter, targetWidth, targetHeight)
fmt.Printf("🔽 IMAGE SERVICE: Scaling to %dx%d with auto-orientation applied\n", targetWidth, targetHeight)
}
fmt.Printf("🎨 IMAGE SERVICE: Using filter chain: %s\n", filterChain)
mergeCmd := exec.Command(
s.ffmpegPath,
"-i", inputPattern,
"-vf", filterChain,
"-frames:v", "1",
"-q:v", quality, // Use the quality setting passed in
"-pix_fmt", "yuvj420p", // Use full-range YUV for better JPEG compatibility
"-y",
outputFile,
)
var mergeOut bytes.Buffer
var mergeErr bytes.Buffer
mergeCmd.Stdout = &mergeOut
mergeCmd.Stderr = &mergeErr
fmt.Printf("🚀 IMAGE SERVICE: Running tile merge: %s %s\n", s.ffmpegPath, strings.Join(mergeCmd.Args[1:], " "))
if err = mergeCmd.Run(); err != nil {
fmt.Printf("❌ IMAGE SERVICE: Tile merge failed: %v\n", err)
fmt.Printf("❌ IMAGE SERVICE: stderr: %s\n", mergeErr.String())
return nil, fmt.Errorf("tile merge failed: %w", err)
}
fmt.Printf("✅ IMAGE SERVICE: Tiles merged successfully\n")
fmt.Printf("📖 IMAGE SERVICE: Reading final merged JPEG file: %s\n", outputFile)
jpegBytes, err := os.ReadFile(outputFile)
if err != nil {
fmt.Printf("❌ IMAGE SERVICE: Failed to read merged JPEG: %v\n", err)
return nil, fmt.Errorf("failed to read merged JPEG: %w", err)
}
fmt.Printf("✅ IMAGE SERVICE: Successfully read %d bytes from merged JPEG\n", len(jpegBytes))
return jpegBytes, nil
}
// ConvertImageToFormat converts any supported image format to another format
func (s *ImageService) ConvertImageToFormat(inputPath, outputFormat string, targetWidth, targetHeight int, quality string) ([]byte, error) {
// Create temporary output file
outputDir := filepath.Dir(inputPath)
ext := ".jpg"
if outputFormat == "png" {
ext = ".png"
}
outputFile := filepath.Join(outputDir, fmt.Sprintf("converted_%d_%d%s", targetWidth, targetHeight, ext))
defer os.Remove(outputFile) // Clean up
// Build FFmpeg command
args := []string{
"-i", inputPath,
}
// Add scaling and auto-orientation
if targetWidth > 0 && targetHeight > 0 {
scaleFilter := fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease,auto-orient=1", targetWidth, targetHeight)
args = append(args, "-vf", scaleFilter)
} else {
args = append(args, "-vf", "auto-orient=1")
}
// Add quality settings
if quality != "" {
args = append(args, "-q:v", quality)
}
// Add output file and overwrite flag
args = append(args, "-y", outputFile)
cmd := exec.Command(s.ffmpegPath, args...)
if s.debug {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("ffmpeg image conversion failed on file '%s': %w", inputPath, err)
}
// Read the converted file
convertedBytes, err := os.ReadFile(outputFile)
if err != nil {
return nil, fmt.Errorf("failed to read converted image: %w", err)
}
return convertedBytes, nil
}
+289
View File
@@ -0,0 +1,289 @@
package ffmpeg
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
"github.com/gtsteffaniak/go-logger/logger"
)
// SubtitleTrack represents a subtitle track (embedded or external file)
type SubtitleTrack struct {
Name string `json:"name"` // filename for external, or descriptive name for embedded
Language string `json:"language,omitempty"` // language code
Title string `json:"title,omitempty"` // title/description
Index *int `json:"index,omitempty"` // stream index for embedded subtitles (nil for external)
Codec string `json:"codec,omitempty"` // codec name for embedded subtitles
Content string `json:"content,omitempty"` // subtitle content
IsFile bool `json:"isFile"` // true for external files, false for embedded streams
}
// FFProbeOutput represents the JSON output from ffprobe
type FFProbeOutput struct {
Streams []struct {
Index int `json:"index"`
CodecType string `json:"codec_type"`
CodecName string `json:"codec_name"`
Tags map[string]string `json:"tags,omitempty"`
Disposition map[string]int `json:"disposition,omitempty"`
} `json:"streams"`
}
// DetectAllSubtitles finds both embedded and external subtitle tracks
func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) []SubtitleTrack {
key := "all_subtitles:" + videoPath + ":" + modtime.Format(time.RFC3339)
// Check cache first
if cached, ok := utils.MediaCache.Get(key).([]SubtitleTrack); ok {
return cached
}
var allSubtitles []SubtitleTrack
// First, get embedded subtitles
embeddedSubs := detectEmbeddedSubtitles(videoPath)
allSubtitles = append(allSubtitles, embeddedSubs...)
// Then, get external subtitle files
externalSubs := detectExternalSubtitles(videoPath, parentDir)
allSubtitles = append(allSubtitles, externalSubs...)
// Cache the complete list
utils.MediaCache.Set(key, allSubtitles)
return allSubtitles
}
// detectEmbeddedSubtitles uses ffprobe to find embedded subtitle tracks
func detectEmbeddedSubtitles(realPath string) []SubtitleTrack {
cmd := exec.Command("ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_streams",
"-select_streams", "s",
realPath)
output, err := cmd.Output()
if err != nil {
logger.Debug("ffprobe failed for file: " + realPath + ", error: " + err.Error())
return nil
}
var probeOutput FFProbeOutput
if err := json.Unmarshal(output, &probeOutput); err != nil {
logger.Debug("failed to parse ffprobe output for file: " + realPath)
return nil
}
var subtitles []SubtitleTrack
for _, stream := range probeOutput.Streams {
if stream.CodecType == "subtitle" {
track := SubtitleTrack{
Index: &stream.Index,
Codec: stream.CodecName,
IsFile: false, // This is an embedded subtitle
}
// Extract language and title from tags
if stream.Tags != nil {
if lang, ok := stream.Tags["language"]; ok {
track.Language = lang
}
if title, ok := stream.Tags["title"]; ok {
track.Title = title
}
}
// Generate a descriptive name
if track.Title != "" {
track.Name = track.Title
} else if track.Language != "" {
track.Name = "Embedded (" + track.Language + ")"
} else {
track.Name = "Embedded Subtitle " + strconv.Itoa(stream.Index)
}
subtitles = append(subtitles, track)
}
}
return subtitles
}
// detectExternalSubtitles finds external subtitle files in the same directory
func detectExternalSubtitles(videoPath string, parentDir string) []SubtitleTrack {
var subtitles []SubtitleTrack
// Get the base name of the video (without extension)
videoBaseName := strings.TrimSuffix(filepath.Base(videoPath), filepath.Ext(videoPath))
// Common subtitle extensions
subtitleExts := []string{".srt", ".vtt", ".lrc", ".sbv", ".ass", ".ssa", ".sub", ".smi"}
// Look for subtitle files with matching base name
for _, ext := range subtitleExts {
subtitlePath := filepath.Join(parentDir, videoBaseName+ext)
if _, err := os.Stat(subtitlePath); err == nil {
// File exists
track := SubtitleTrack{
Name: filepath.Base(subtitlePath),
IsFile: true, // This is an external file
}
// Try to infer language from filename patterns like "video.en.srt"
parts := strings.Split(videoBaseName, ".")
if len(parts) > 1 {
// Check if the last part before extension looks like a language code
lastPart := parts[len(parts)-1]
if len(lastPart) == 2 || len(lastPart) == 3 {
track.Language = lastPart
}
}
subtitles = append(subtitles, track)
}
}
return subtitles
}
// ExtractSubtitleContent extracts subtitle content without service management
func ExtractSubtitleContent(videoPath string, streamIndex int) (string, error) {
cmd := exec.Command("ffmpeg",
"-i", videoPath,
"-map", fmt.Sprintf("0:%d", streamIndex),
"-c:s", "webvtt",
"-f", "webvtt",
"-") // output to stdout
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("subtitle extraction failed: %v", err)
}
return string(output), nil
}
// LoadAndConvertSubtitleFile loads a subtitle file and converts it to WebVTT format
func LoadAndConvertSubtitleFile(subtitlePath string) (string, error) {
ext := strings.ToLower(filepath.Ext(subtitlePath))
if ext == ".vtt" {
// Already WebVTT, just read it
content, err := os.ReadFile(subtitlePath)
if err != nil {
return "", err
}
return string(content), nil
}
if ext == ".srt" {
// Convert SRT to WebVTT using ffmpeg
cmd := exec.Command("ffmpeg",
"-i", subtitlePath,
"-c:s", "webvtt",
"-f", "webvtt",
"-") // output to stdout
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to convert SRT to WebVTT: %v", err)
}
return string(output), nil
}
// For other formats, try to read as plain text and hope for the best
// This is a fallback - ideally you'd want format-specific converters
content, err := os.ReadFile(subtitlePath)
if err != nil {
return "", err
}
// Basic conversion wrapper for non-VTT formats
vttHeader := "WEBVTT\n\n"
return vttHeader + string(content), nil
}
// ExtractSingleSubtitle extracts content for a specific subtitle track by array index
func ExtractSingleSubtitle(videoPath string, parentDir string, trackIndex int, modtime time.Time) (SubtitleTrack, error) {
// Get all subtitle tracks
allTracks := DetectAllSubtitles(videoPath, parentDir, modtime)
if trackIndex >= len(allTracks) {
return SubtitleTrack{}, fmt.Errorf("subtitle track %d not found (only %d tracks available)", trackIndex, len(allTracks))
}
track := allTracks[trackIndex]
// Load content based on type
if track.IsFile {
// Load external subtitle file
subtitlePath := filepath.Join(parentDir, track.Name)
content, err := LoadAndConvertSubtitleFile(subtitlePath)
if err != nil {
return SubtitleTrack{}, fmt.Errorf("failed to load external subtitle: %v", err)
}
track.Content = content
} else {
// Extract embedded subtitle content
if track.Index == nil {
return SubtitleTrack{}, fmt.Errorf("embedded subtitle track has no stream index")
}
content, err := ExtractSubtitleContent(videoPath, *track.Index)
if err != nil {
return SubtitleTrack{}, fmt.Errorf("failed to extract embedded subtitle: %v", err)
}
track.Content = content
}
return track, nil
}
// LoadAllSubtitleContent loads the actual content for all detected subtitle tracks
func LoadAllSubtitleContent(videoPath string, subtitles []SubtitleTrack, modtime time.Time) error {
for idx := range subtitles {
subtitle := &subtitles[idx]
// Check if content is already cached
contentKey := fmt.Sprintf("subtitle_content:%s:%d:%s", videoPath, idx, modtime.Format(time.RFC3339))
if cached, ok := utils.MediaCache.Get(contentKey).(string); ok {
subtitle.Content = cached
continue
}
var content string
var err error
if subtitle.IsFile {
// Load external subtitle file content and convert to WebVTT
subtitlePath := filepath.Join(filepath.Dir(videoPath), subtitle.Name)
content, err = LoadAndConvertSubtitleFile(subtitlePath)
if err != nil {
logger.Debug("failed to read/convert subtitle file " + subtitlePath + ": " + err.Error())
continue
}
} else {
// Load embedded subtitle content
if subtitle.Index == nil {
logger.Debug("embedded subtitle has no stream index")
continue
}
content, err = ExtractSubtitleContent(videoPath, *subtitle.Index)
if err != nil {
logger.Debug("failed to extract embedded subtitle: " + err.Error())
continue
}
}
subtitle.Content = content
// Cache the content for future requests
utils.MediaCache.Set(contentKey, content)
}
return nil
}
+147
View File
@@ -0,0 +1,147 @@
package ffmpeg
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/gtsteffaniak/go-logger/logger"
)
// VideoService handles video preview operations with ffmpeg
type VideoService struct {
ffmpegPath string
ffprobePath string
debug bool
semaphore chan struct{}
}
// NewVideoService creates a new video service instance
func NewVideoService(ffmpegPath, ffprobePath string, maxConcurrent int, debug bool) *VideoService {
return &VideoService{
ffmpegPath: ffmpegPath,
ffprobePath: ffprobePath,
debug: debug,
semaphore: make(chan struct{}, maxConcurrent),
}
}
func (s *VideoService) acquire(ctx context.Context) error {
select {
case s.semaphore <- struct{}{}:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (s *VideoService) release() {
<-s.semaphore
}
// GenerateVideoPreview generates a single preview image from a video using ffmpeg.
// videoPath: path to the input video file.
// outputPath: path where the generated preview image will be saved (e.g., "/tmp/preview.jpg").
// seekTime: how many seconds into the video to seek before capturing the frame.
func (s *VideoService) GenerateVideoPreview(videoPath, outputPath string, percentageSeek int) ([]byte, error) {
if err := s.acquire(context.Background()); err != nil {
return nil, err
}
defer s.release()
// Step 1: Get video duration from the container format
probeCmd := exec.Command(
s.ffprobePath,
"-v", "error",
// Use format=duration for better compatibility
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
videoPath,
)
var probeOut bytes.Buffer
probeCmd.Stdout = &probeOut
if s.debug {
probeCmd.Stderr = os.Stderr
}
if err := probeCmd.Run(); err != nil {
logger.Errorf("ffprobe command failed on file '%v' : %v", videoPath, err)
return nil, fmt.Errorf("ffprobe failed: %w", err)
}
durationStr := strings.TrimSpace(probeOut.String())
if durationStr == "" || durationStr == "N/A" {
logger.Errorf("could not determine video duration for file '%v' using duration info '%v'", videoPath, durationStr)
return nil, fmt.Errorf("could not determine video duration")
}
durationFloat, err := strconv.ParseFloat(durationStr, 64)
if err != nil {
// The original error you saw would be caught here if "N/A" was still the output
return nil, fmt.Errorf("invalid duration: %v", err)
}
if durationFloat <= 0 {
return nil, fmt.Errorf("video duration must be positive")
}
// Step 2: Get the duration of the video in whole seconds
duration := int(durationFloat)
// Step 3: Calculate seek time based on percentageSeek (percentage value)
seekSeconds := duration * percentageSeek / 100
// Step 4: Convert seekSeconds to string for ffmpeg command
seekTime := strconv.Itoa(seekSeconds)
// Step 5: Extract frame at seek time
cmd := exec.Command(
s.ffmpegPath,
"-ss", seekTime,
"-i", videoPath,
"-frames:v", "1",
"-q:v", "10",
"-y", // overwrite output
outputPath,
)
if s.debug {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err = cmd.Run()
if err != nil {
return nil, fmt.Errorf("ffmpeg command failed on file '%v' : %w", videoPath, err)
}
return os.ReadFile(outputPath)
}
// GetVideoDuration extracts the duration of a video file using ffprobe
func GetVideoDuration(ffprobePath string, videoPath string) (float64, error) {
cmd := exec.Command(ffprobePath,
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
videoPath)
output, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("ffprobe failed: %w", err)
}
durationStr := strings.TrimSpace(string(output))
if durationStr == "" || durationStr == "N/A" {
return 0, fmt.Errorf("could not determine video duration")
}
duration, err := strconv.ParseFloat(durationStr, 64)
if err != nil {
return 0, fmt.Errorf("invalid duration: %v", err)
}
return duration, nil
}
+80 -56
View File
@@ -3,6 +3,7 @@ module github.com/gtsteffaniak/filebrowser/backend
go 1.25.0
require (
github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786
github.com/asdine/storm/v3 v3.2.1
github.com/coreos/go-oidc/v3 v3.15.0
github.com/dsoprea/go-exif/v3 v3.0.1
@@ -15,53 +16,62 @@ require (
github.com/gtsteffaniak/go-logger v0.1.2
github.com/kovidgoyal/imaging v1.6.4
github.com/pquerna/otp v1.5.0
github.com/spf13/afero v1.14.0
github.com/stretchr/testify v1.10.0
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.41.0
golang.org/x/image v0.30.0
golang.org/x/mod v0.27.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.35.0
golang.org/x/time v0.12.0
golang.org/x/crypto v0.42.0
golang.org/x/image v0.31.0
golang.org/x/mod v0.28.0
golang.org/x/oauth2 v0.31.0
golang.org/x/sys v0.36.0
golang.org/x/time v0.13.0
gopkg.in/yaml.v3 v3.0.1
)
require (
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
github.com/4meepo/tagalign v1.4.2 // indirect
github.com/Abirdcfly/dupword v0.1.3 // indirect
codeberg.org/chavacava/garif v0.2.0 // indirect
dario.cat/mergo v1.0.2 // indirect
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
dev.gaijin.team/go/golib v0.6.0 // indirect
github.com/4meepo/tagalign v1.4.3 // indirect
github.com/Abirdcfly/dupword v0.1.6 // indirect
github.com/AlwxSin/noinlineerr v1.0.5 // indirect
github.com/Antonboom/errname v1.1.0 // indirect
github.com/Antonboom/nilnil v1.1.0 // indirect
github.com/Antonboom/testifylint v1.6.1 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/alecthomas/chroma/v2 v2.17.2 // indirect
github.com/air-verse/air v1.63.0 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alfatraining/structtag v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/alingse/nilnesserr v0.2.0 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.2.0 // indirect
github.com/ashanbrown/forbidigo/v2 v2.1.0 // indirect
github.com/ashanbrown/makezero/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bep/godartsass/v2 v2.5.0 // indirect
github.com/bep/golibsass v1.2.0 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
github.com/bombsimon/wsl/v5 v5.1.1 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/breml/bidichk v0.3.3 // indirect
github.com/breml/errchkjson v0.4.1 // indirect
github.com/butuzov/ireturn v0.4.0 // indirect
github.com/butuzov/mirror v1.3.0 // indirect
github.com/catenacyber/perfsprint v0.9.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charithe/durationcheck v0.0.10 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
@@ -69,11 +79,11 @@ require (
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/chavacava/garif v0.1.0 // indirect
github.com/ckaznocha/intrange v0.3.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.13.6 // indirect
github.com/daixiang0/gci v0.13.7 // indirect
github.com/dave/dst v0.27.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
@@ -85,17 +95,28 @@ require (
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.6 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/ghostiam/protogetter v0.3.15 // indirect
github.com/go-critic/go-critic v0.13.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/jsonpointer v0.22.0 // indirect
github.com/go-openapi/jsonreference v0.21.1 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag v0.24.1 // indirect
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
github.com/go-openapi/swag/conv v0.24.0 // indirect
github.com/go-openapi/swag/fileutils v0.24.0 // indirect
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
github.com/go-openapi/swag/jsonutils v0.24.0 // indirect
github.com/go-openapi/swag/loading v0.24.0 // indirect
github.com/go-openapi/swag/mangling v0.24.0 // indirect
github.com/go-openapi/swag/netutils v0.24.0 // indirect
github.com/go-openapi/swag/stringutils v0.24.0 // indirect
github.com/go-openapi/swag/typeutils v0.24.0 // indirect
github.com/go-openapi/swag/yamlutils v0.24.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
@@ -105,20 +126,22 @@ require (
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac // indirect
github.com/gohugoio/hugo v0.149.1 // indirect
github.com/golang/geo v0.0.0-20250909101022-21d0664d86be // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.0 // indirect
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
github.com/golangci/golangci-lint/v2 v2.1.6 // indirect
github.com/golangci/golangci-lint/v2 v2.4.0 // indirect
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect
github.com/golangci/misspell v0.6.0 // indirect
github.com/golangci/plugin-module-register v0.1.1 // indirect
github.com/golangci/misspell v0.7.0 // indirect
github.com/golangci/plugin-module-register v0.1.2 // indirect
github.com/golangci/revgrep v0.8.0 // indirect
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
@@ -131,9 +154,9 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.8.1 // indirect
github.com/jgautheron/goconst v1.8.2 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jjti/go-spancheck v0.6.4 // indirect
github.com/jjti/go-spancheck v0.6.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/julz/importas v0.2.0 // indirect
github.com/jupiterrider/ffi v0.5.1 // indirect
@@ -143,18 +166,19 @@ require (
github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.14 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
github.com/ldez/exptostd v0.4.3 // indirect
github.com/ldez/gomoddirectives v0.6.1 // indirect
github.com/ldez/grignotin v0.9.0 // indirect
github.com/ldez/exptostd v0.4.4 // indirect
github.com/ldez/gomoddirectives v0.7.0 // indirect
github.com/ldez/grignotin v0.10.0 // indirect
github.com/ldez/tagliatelle v0.7.1 // indirect
github.com/ldez/usetesting v0.4.3 // indirect
github.com/ldez/usetesting v0.5.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/macabu/inamedparam v0.2.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/manuelarte/funcorder v0.2.1 // indirect
github.com/manuelarte/embeddedstructfieldcheck v0.3.0 // indirect
github.com/manuelarte/funcorder v0.5.0 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v1.1.0 // indirect
@@ -162,16 +186,15 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgechev/revive v1.9.0 // indirect
github.com/mgechev/revive v1.11.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/nunnatsa/ginkgolinter v0.20.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -189,21 +212,22 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect
github.com/securego/gosec/v2 v2.22.3 // indirect
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
github.com/securego/gosec/v2 v2.22.7 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.1.0 // indirect
github.com/sonatard/noctx v0.4.0 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/spf13/viper v1.12.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
@@ -211,6 +235,7 @@ require (
github.com/subosito/gotenv v1.4.1 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/tdakkota/asciicheck v0.4.1 // indirect
github.com/tdewolff/parse/v2 v2.8.3 // indirect
github.com/tetafro/godot v1.5.1 // indirect
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
github.com/timonwong/loggercheck v0.11.0 // indirect
@@ -220,7 +245,7 @@ require (
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
github.com/uudashr/iface v1.3.1 // indirect
github.com/uudashr/iface v1.4.1 // indirect
github.com/xen0n/gosmopolitan v1.3.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
@@ -228,21 +253,19 @@ require (
github.com/ykadowak/zerologlint v0.1.5 // indirect
gitlab.com/bosi/decorder v0.4.2 // indirect
go-simpler.org/musttag v0.13.1 // indirect
go-simpler.org/sloglint v0.11.0 // indirect
go-simpler.org/sloglint v0.11.1 // indirect
go.augendre.info/arangolint v0.2.0 // indirect
go.augendre.info/fatcontext v0.8.0 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/text v0.28.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
google.golang.org/protobuf v1.36.6 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.6.1 // indirect
@@ -252,6 +275,7 @@ require (
)
tool (
github.com/air-verse/air
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
github.com/swaggo/swag/cmd/swag
)
+277 -145
View File
@@ -34,17 +34,29 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY=
codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=
dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=
dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo=
dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw=
github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8=
github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
github.com/Abirdcfly/dupword v0.1.6 h1:qeL6u0442RPRe3mcaLcbaCi2/Y/hOcdtw6DE9odjz9c=
github.com/Abirdcfly/dupword v0.1.6/go.mod h1:s+BFMuL/I4YSiFv29snqyjwzDp4b65W2Kvy+PKzZ6cw=
github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY=
github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
github.com/Antonboom/errname v1.1.0 h1:A+ucvdpMwlo/myWrkHEUEBWc/xuXdud23S8tmTb/oAE=
github.com/Antonboom/errname v1.1.0/go.mod h1:O1NMrzgUcVBGIfi3xlVuvX8Q/VP/73sseCaAppfjqZw=
github.com/Antonboom/nilnil v1.1.0 h1:jGxJxjgYS3VUUtOTNk8Z1icwT5ESpLH/426fjmQG+ng=
github.com/Antonboom/nilnil v1.1.0/go.mod h1:b7sAlogQjFa1wV8jUW3o4PMzDVFLbTux+xnQdvzdcIE=
github.com/Antonboom/testifylint v1.6.1 h1:6ZSytkFWatT8mwZlmRCHkWz1gPi+q6UBSbieji2Gj/o=
github.com/Antonboom/testifylint v1.6.1/go.mod h1:k+nEkathI2NFjKO6HvwmSrbzUcQ6FAnbZV+ZRrnXPLI=
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
@@ -53,8 +65,6 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
@@ -63,14 +73,16 @@ github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsu
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/air-verse/air v1.63.0 h1:fwcdHpwaUe4/+q349PxptzAIn8gVE6Yke8TgW0LsDxQ=
github.com/air-verse/air v1.63.0/go.mod h1:RyCQVx2+3Zz2BzoqkukYiGmWkWXNKMf0x5ubIFcUB8Q=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -80,30 +92,64 @@ github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQ
github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus=
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY=
github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU=
github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU=
github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4=
github.com/ashanbrown/forbidigo/v2 v2.1.0 h1:NAxZrWqNUQiDz19FKScQ/xvwzmij6BiOw3S0+QUQ+Hs=
github.com/ashanbrown/forbidigo/v2 v2.1.0/go.mod h1:0zZfdNAuZIL7rSComLGthgc/9/n2FqspBOH90xlCHdA=
github.com/ashanbrown/makezero/v2 v2.0.1 h1:r8GtKetWOgoJ4sLyUx97UTwyt2dO7WkGFHizn/Lo8TY=
github.com/ashanbrown/makezero/v2 v2.0.1/go.mod h1:kKU4IMxmYW1M4fiEHMb2vc5SFoPzXvgbMR9gIp5pjSw=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc=
github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo=
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU=
github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI=
github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5kujw=
github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg=
github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q=
github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc=
github.com/bep/helpers v0.6.0 h1:qtqMCK8XPFNM9hp5Ztu9piPjxNNkk8PIyUVjg6v8Bsw=
github.com/bep/helpers v0.6.0/go.mod h1:IOZlgx5PM/R/2wgyCatfsgg5qQ6rNZJNDpWGXqDR044=
github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k=
github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8=
github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk=
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0=
github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE=
github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro=
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
github.com/bombsimon/wsl/v5 v5.1.1 h1:cQg5KJf9FlctAH4cpL9vLKnziYknoCMCdqXl0wjl72Q=
github.com/bombsimon/wsl/v5 v5.1.1/go.mod h1:Gp8lD04z27wm3FANIUPZycXp+8huVsn0oxc+n4qfV9I=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
@@ -117,8 +163,8 @@ github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=
github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=
github.com/catenacyber/perfsprint v0.9.1 h1:5LlTp4RwTooQjJCvGEFV6XksZvWE7wCOUvjD2z0vls0=
github.com/catenacyber/perfsprint v0.9.1/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM=
github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=
github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=
github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -136,13 +182,13 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
@@ -150,10 +196,12 @@ github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmr
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
github.com/daixiang0/gci v0.13.6 h1:RKuEOSkGpSadkGbvZ6hJ4ddItT3cVZ9Vn9Rybk6xjl8=
github.com/daixiang0/gci v0.13.6/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk=
github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ=
github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ=
github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
@@ -163,6 +211,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
@@ -190,22 +240,29 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
github.com/evanw/esbuild v0.25.9 h1:aU7GVC4lxJGC1AyaPwySWjSIaNLAdVEEuq3chD0Khxs=
github.com/evanw/esbuild v0.25.9/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E=
github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY=
github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY=
@@ -227,16 +284,38 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
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.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
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=
@@ -269,10 +348,12 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
@@ -280,13 +361,29 @@ github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg=
github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec=
github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs=
github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
github.com/gohugoio/hugo v0.149.1 h1:uWOc8Ve4h4e48FyYhBquRoHCJviyxA5yGrFJLT48yio=
github.com/gohugoio/hugo v0.149.1/go.mod h1:HS6BP6e8FGxungP4CHC3zeLDvhBLnTJIjHJZWTZjs7o=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0 h1:dco+7YiOryRoPOMXwwaf+kktZSCtlFtreNdiJbETvYE=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0/go.mod h1:CRrxQTKeM3imw+UoS4EHKyrqB7Zp6sAJiqHit+aMGTE=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1 h1:nUzXfRTszLliZuN0JTKeunXTRaiFX6ksaWP0puLLYAY=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1/go.mod h1:Wy8ThAA8p2/w1DY05vEzq6EIeI2mzDjvHsu7ULBVwog=
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
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-20250821133510-ecfc33a939ac h1:gBtvd74wZT02fkOyoG651Sx2imv5lKdVvDwpUjKPFnw=
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/geo v0.0.0-20250909101022-21d0664d86be h1:LYVqvM1o7RQN4bdv0iQbd7+x2aQ5m0mi5aQp5r/oR9U=
github.com/golang/geo v0.0.0-20250909101022-21d0664d86be/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
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=
@@ -324,16 +421,18 @@ github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUP
github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
github.com/golangci/golangci-lint/v2 v2.1.6 h1:LXqShFfAGM5BDzEOWD2SL1IzJAgUOqES/HRBsfKjI+w=
github.com/golangci/golangci-lint/v2 v2.1.6/go.mod h1:EPj+fgv4TeeBq3TcqaKZb3vkiV5dP4hHHKhXhEhzci8=
github.com/golangci/golangci-lint/v2 v2.4.0 h1:qz6O6vr7kVzXJqyvHjHSz5fA3D+PM8v96QU5gxZCNWM=
github.com/golangci/golangci-lint/v2 v2.4.0/go.mod h1:Oq7vuAf6L1iNL34uHDcsIF6Mnc0amOPdsT3/GlpHD+I=
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8=
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ=
github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc=
github.com/golangci/misspell v0.7.0 h1:4GOHr/T1lTW0hhR4tgaaV1WS/lJ+ncvYCoFKmqJsj0c=
github.com/golangci/misspell v0.7.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg=
github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -348,7 +447,6 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -362,13 +460,15 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18=
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
@@ -386,6 +486,8 @@ github.com/gtsteffaniak/go-cache v0.0.0-20250521142451-edc77dfcb063 h1:CoLbeqTmM
github.com/gtsteffaniak/go-cache v0.0.0-20250521142451-edc77dfcb063/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
github.com/gtsteffaniak/go-logger v0.1.2 h1:0g6UxxNwU+Q+7IVRw+Yn7AuYsIK0ukiTgbzjTLm+Uy4=
github.com/gtsteffaniak/go-logger v0.1.2/go.mod h1:U3ZkdAclcxXNMvoQLq+zDkqcTmdqhX3hTf+6AuTo6v0=
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -404,14 +506,16 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jgautheron/goconst v1.8.1 h1:PPqCYp3K/xlOj5JmIe6O1Mj6r1DbkdbLtR3AJuZo414=
github.com/jgautheron/goconst v1.8.1/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako=
github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4=
github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako=
github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs=
github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=
github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc=
github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk=
github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -450,18 +554,20 @@ github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs=
github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I=
github.com/kunwardeep/paralleltest v1.0.14 h1:wAkMoMeGX/kGfhQBPODT/BL8XhK23ol/nuQ3SwFaUw8=
github.com/kunwardeep/paralleltest v1.0.14/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4=
github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=
github.com/ldez/exptostd v0.4.3 h1:Ag1aGiq2epGePuRJhez2mzOpZ8sI9Gimcb4Sb3+pk9Y=
github.com/ldez/exptostd v0.4.3/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ=
github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc=
github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs=
github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow=
github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk=
github.com/ldez/exptostd v0.4.4 h1:58AtQjnLcT/tI5W/1KU7xE/O7zW9RAWB6c/ScQAnfus=
github.com/ldez/exptostd v0.4.4/go.mod h1:QfdzPw6oHjFVdNV7ILoPu5sw3OZ3OG1JS0I5JN3J4Js=
github.com/ldez/gomoddirectives v0.7.0 h1:EOx8Dd56BZYSez11LVgdj025lKwlP0/E5OLSl9HDwsY=
github.com/ldez/gomoddirectives v0.7.0/go.mod h1:wR4v8MN9J8kcwvrkzrx6sC9xe9Cp68gWYCsda5xvyGc=
github.com/ldez/grignotin v0.10.0 h1:NQPeh1E/Eza4F0exCeC1WkpnLvgUcQDT8MQ1vOLML0E=
github.com/ldez/grignotin v0.10.0/go.mod h1:oR4iCKUP9fwoeO6vCQeD7M5SMxCT6xdVas4vg0h1LaI=
github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk=
github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I=
github.com/ldez/usetesting v0.4.3 h1:pJpN0x3fMupdTf/IapYjnkhiY1nSTN+pox1/GyBRw3k=
github.com/ldez/usetesting v0.4.3/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ=
github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
@@ -474,12 +580,18 @@ github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamh
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/manuelarte/funcorder v0.2.1 h1:7QJsw3qhljoZ5rH0xapIvjw31EcQeFbF31/7kQ/xS34=
github.com/manuelarte/funcorder v0.2.1/go.mod h1:BQQ0yW57+PF9ZpjpeJDKOffEsQbxDFKW8F8zSMe/Zd0=
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
github.com/manuelarte/embeddedstructfieldcheck v0.3.0 h1:VhGqK8gANDvFYDxQkjPbv7/gDJtsGU9k6qj/hC2hgso=
github.com/manuelarte/embeddedstructfieldcheck v0.3.0/go.mod h1:LSo/IQpPfx1dXMcX4ibZCYA7Yy6ayZHIaOGM70+1Wy8=
github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8=
github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA=
github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI=
github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE=
github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04=
github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc=
github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
@@ -488,42 +600,57 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgechev/revive v1.9.0 h1:8LaA62XIKrb8lM6VsBSQ92slt/o92z5+hTw3CmrvSrM=
github.com/mgechev/revive v1.9.0/go.mod h1:LAPq3+MgOf7GcL5PlWIkHb0PT7XH4NuC2LdWymhb9Mo=
github.com/mgechev/revive v1.11.0 h1:b/gLLpBE427o+Xmd8G58gSA+KtBwxWinH/A565Awh0w=
github.com/mgechev/revive v1.11.0/go.mod h1:tI0oLF/2uj+InHCBLrrqfTKfjtFTBCFFfG05auyzgdw=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg=
github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4=
github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/nunnatsa/ginkgolinter v0.20.0 h1:OmWLkAFO2HUTYcU6mprnKud1Ey5pVdiVNYGO5HVicx8=
github.com/nunnatsa/ginkgolinter v0.20.0/go.mod h1:dCIuFlTPfQerXgGUju3VygfAFPdC5aE1mdacCDKDJcQ=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
@@ -531,10 +658,14 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -590,20 +721,22 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=
github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ=
github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8=
github.com/securego/gosec/v2 v2.22.3 h1:mRrCNmRF2NgZp4RJ8oJ6yPJ7G4x6OCiAXHd8x4trLRc=
github.com/securego/gosec/v2 v2.22.3/go.mod h1:42M9Xs0v1WseinaB/BmNGO8AVqG8vRfhC2686ACY48k=
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
github.com/securego/gosec/v2 v2.22.7 h1:8/9P+oTYI4yIpAzccQKVsg1/90Po+JzGtAhqoHImDeM=
github.com/securego/gosec/v2 v2.22.7/go.mod h1:510TFNDMrIPytokyHQAVLvPeDr41Yihn2ak8P+XQfNE=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
@@ -616,21 +749,22 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM=
github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c=
github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o=
github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
@@ -639,20 +773,14 @@ github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8B
github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
@@ -663,12 +791,20 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=
github.com/tdewolff/minify/v2 v2.24.2 h1:vnY3nTulEAbCAAlxTxPPDkzG24rsq31SOzp63yT+7mo=
github.com/tdewolff/minify/v2 v2.24.2/go.mod h1:1JrCtoZXaDbqioQZfk3Jdmr0GPJKiU7c1Apmb+7tCeE=
github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I=
github.com/tdewolff/parse/v2 v2.8.3/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4=
github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk=
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=
@@ -685,10 +821,12 @@ github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA=
github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=
github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U=
github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg=
github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU=
github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
@@ -706,14 +844,20 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=
go-simpler.org/musttag v0.13.1 h1:lw2sJyu7S1X8lc8zWUAdH42y+afdcCnHhWpnkWvd6vU=
go-simpler.org/musttag v0.13.1/go.mod h1:8r450ehpMLQgvpb6sg+hV5Ur47eH6olp/3yEanfG97k=
go-simpler.org/sloglint v0.11.0 h1:JlR1X4jkbeaffiyjLtymeqmGDKBDO1ikC6rjiuFAOco=
go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkyeW2Pvw=
go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s=
go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ=
go.augendre.info/arangolint v0.2.0 h1:2NP/XudpPmfBhQKX4rMk+zDYIj//qbt4hfZmSSTcpj8=
go.augendre.info/arangolint v0.2.0/go.mod h1:Vx4KSJwu48tkE+8uxuf0cbBnAPgnt8O1KWiT7bljq7w=
go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk=
go.augendre.info/fatcontext v0.8.0/go.mod h1:oVJfMgwngMsHO+KB2MdgzcO+RvtNdiCEOlWvSFtax/s=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
@@ -724,16 +868,14 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -743,8 +885,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -755,16 +897,16 @@ 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-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
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-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo=
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -787,13 +929,11 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -831,23 +971,21 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -863,8 +1001,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -908,25 +1046,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
@@ -937,18 +1071,16 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -1000,9 +1132,7 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
@@ -1092,8 +1222,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1129,6 +1259,8 @@ mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg=
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 h1:WjUu4yQoT5BHT1w8Zu56SP8367OuBV5jvo+4Ulppyf8=
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4/go.mod h1:rthT7OuvRbaGcd5ginj6dA2oLE7YNlta9qhBNNdCaLE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+5 -1
View File
@@ -71,7 +71,11 @@ func sseHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
return http.StatusInternalServerError, fmt.Errorf("error sending broadcast: %v, user: %s", err, username)
}
case msg := <-sendChan:
case msg, ok := <-sendChan:
if !ok {
logger.Debugf("SSE channel closed for user: %s, SessionId: %s", username, sessionId)
return http.StatusOK, nil
}
if err := msgr.sendEvent(msg.EventType, msg.Message); err != nil {
return http.StatusInternalServerError, fmt.Errorf("error sending targeted message: %v, user: %s", err, username)
}
+43 -65
View File
@@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"embed"
"encoding/json"
"fmt"
"io/fs"
"net/http"
@@ -14,6 +15,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/common/version"
"github.com/gtsteffaniak/filebrowser/backend/database/storage/bolt"
"github.com/gtsteffaniak/filebrowser/backend/events"
"github.com/gtsteffaniak/go-logger/logger"
// http-swagger middleware
)
@@ -23,9 +25,6 @@ import (
//go:embed embed/*
var assets embed.FS
// Boolean flag to determine whether to use the embedded FS or not
var embeddedFS = os.Getenv("FILEBROWSER_NO_EMBEDED") != "true"
// Custom dirFS to handle both embedded and non-embedded file systems
type dirFS struct {
http.Dir
@@ -45,7 +44,18 @@ var (
func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete chan struct{}) {
store = storage
config = &settings.Config
var err error
// Dev mode enables development features like template hot-reloading
devMode := os.Getenv("FILEBROWSER_DEVMODE") == "true"
_, err := os.Stat("http/dist")
// In dev mode, always use filesystem assets. Otherwise, check if http/dist exists
var embeddedFS bool
if devMode {
embeddedFS = false
} else {
embeddedFS = os.IsNotExist(err)
}
// --- START: ADD THIS DECRYPTION LOGIC ---
if embeddedFS {
// Embedded mode: Serve files from the embedded assets
@@ -57,8 +67,20 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
assetFs = dirFS{Dir: http.Dir("http/dist")}
}
// In development mode, we want to reload the templates on each request.
// In production (embedded), we parse them once.
templates := template.New("").Funcs(template.FuncMap{
"marshal": func(v interface{}) (string, error) {
a, err := json.Marshal(v)
return string(a), err
},
})
if !devMode {
templates = template.Must(templates.ParseFS(assetFs, "public/index.html"))
}
templateRenderer = &TemplateRenderer{
templates: template.Must(template.ParseFS(assetFs, "public/index.html")),
templates: templates,
devMode: devMode,
}
router := http.NewServeMux()
@@ -95,6 +117,7 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
api.HandleFunc("PATCH /resources", withUser(resourcePatchHandler))
api.HandleFunc("GET /raw", withUser(rawHandler))
api.HandleFunc("GET /preview", withUser(previewHandler))
api.HandleFunc("GET /media/subtitles", withUser(subtitlesHandler))
if version.Version == "testing" || version.Version == "untracked" {
api.HandleFunc("GET /inspectIndex", inspectIndex)
api.HandleFunc("GET /mockData", mockData)
@@ -106,22 +129,28 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
api.HandleFunc("GET /access/groups", withAdmin(groupGetHandler))
api.HandleFunc("POST /access/group", withAdmin(groupPostHandler))
api.HandleFunc("DELETE /access/group", withAdmin(groupDeleteHandler))
// Share management routes (require permission)
api.HandleFunc("GET /shares", withPermShare(shareListHandler))
api.HandleFunc("GET /share/direct", withPermShare(shareDirectDownloadHandler))
api.HandleFunc("GET /share", withPermShare(shareGetHandler))
api.HandleFunc("POST /share", withPermShare(sharePostHandler))
api.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler))
// Create API sub-router for public API endpoints
publicAPI := http.NewServeMux()
// NEW PUBLIC ROUTES - All publicly accessible endpoints
// Share management routes (require permission but are publicly accessible)
publicRoutes.HandleFunc("GET /shares", withPermShare(shareListHandler))
publicRoutes.HandleFunc("GET /share/direct", withPermShare(shareDirectDownloadHandler))
publicRoutes.HandleFunc("GET /share", withPermShare(shareGetHandler))
publicRoutes.HandleFunc("POST /share", withPermShare(sharePostHandler))
publicRoutes.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler))
// Public API routes (hash-based authentication)
publicAPI.HandleFunc("GET /raw", withHashFile(publicRawHandler))
publicAPI.HandleFunc("GET /preview", withHashFile(publicPreviewHandler))
publicAPI.HandleFunc("GET /resources", withHashFile(publicShareHandler))
publicAPI.HandleFunc("GET /users", withUser(userGetHandler))
publicAPI.HandleFunc("POST /onlyoffice/callback", withHashFile(onlyofficeCallbackHandler))
publicAPI.HandleFunc("GET /onlyoffice/config", withHashFile(onlyofficeClientConfigGetHandler))
// Settings routes
api.HandleFunc("GET /settings", withAdmin(settingsGetHandler))
api.HandleFunc("GET /settings/config", withAdmin(settingsConfigHandler))
// Events routes
api.HandleFunc("GET /events", withUser(func(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
@@ -132,63 +161,9 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
api.HandleFunc("GET /jobs/{action}/{target}", withUser(getJobsHandler))
api.HandleFunc("GET /onlyoffice/config", withUser(onlyofficeClientConfigGetHandler))
publicAPI.HandleFunc("GET /onlyoffice/config", withHashFile(onlyofficeClientConfigGetHandler))
api.HandleFunc("POST /onlyoffice/callback", withUser(onlyofficeCallbackHandler))
publicAPI.HandleFunc("POST /onlyoffice/callback", withHashFile(onlyofficeCallbackHandler))
api.HandleFunc("GET /onlyoffice/getToken", withUser(onlyofficeGetTokenHandler))
publicAPI.HandleFunc("GET /onlyoffice/getToken", withHashFile(onlyofficeGetTokenHandler))
api.HandleFunc("GET /search", withUser(searchHandler))
// Share routes (DEPRECATED - maintain for backwards compatibility)
// These will redirect to the new /public/shares endpoints
api.HandleFunc("GET /shares", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/shares"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
api.HandleFunc("GET /share", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/share"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
api.HandleFunc("POST /share", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, config.Server.BaseURL+"public/share", http.StatusPermanentRedirect)
})
api.HandleFunc("DELETE /share", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/share"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusPermanentRedirect)
})
api.HandleFunc("GET /public/raw", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/api/raw"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
api.HandleFunc("GET /public/share", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/api/shared"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
api.HandleFunc("GET /public/preview", func(w http.ResponseWriter, r *http.Request) {
newURL := config.Server.BaseURL + "public/api/preview"
if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery
}
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
})
// Mount the public API sub-router
publicRoutes.Handle("/api/", http.StripPrefix("/api", publicAPI))
@@ -281,6 +256,9 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
<-ctx.Done()
logger.Info("Shutting down HTTP server...")
// Close all SSE sessions
events.Shutdown()
// Persist in-memory state before shutting down the HTTP server
if store != nil {
if store.Share != nil {
+83
View File
@@ -0,0 +1,83 @@
package http
import (
"bytes"
"fmt"
"net/http"
"net/url"
"path/filepath"
"strconv"
"time"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
"github.com/gtsteffaniak/filebrowser/backend/indexing"
)
// subtitlesHandler handles subtitle extraction requests
// @Summary Extract embedded subtitles
// @Description Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content
// @Tags Subtitles
// @Accept json
// @Produce text/vtt
// @Param path query string true "Index path to the video file"
// @Param source query string true "Source name for the desired source"
// @Param index query int false "Stream index for embedded subtitle extraction, defaults to 0"
// @Success 200 {string} string "Raw WebVTT subtitle content"
// @Failure 400 {object} map[string]string "Bad request"
// @Failure 403 {object} map[string]string "Forbidden"
// @Failure 404 {object} map[string]string "Resource not found"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/media/subtitles [get]
func subtitlesHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
encodedPath := r.URL.Query().Get("path")
source := r.URL.Query().Get("source")
indexParam := r.URL.Query().Get("index")
if indexParam == "" {
indexParam = "0" // default to first subtitle stream
}
path, err := url.QueryUnescape(encodedPath)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
source, err = url.QueryUnescape(source)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid source encoding: %v", err)
}
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
if err != nil {
return http.StatusForbidden, err
}
idx := indexing.GetIndex(source)
if idx == nil {
return http.StatusNotFound, fmt.Errorf("source %s not found", source)
}
realPath, _, err := idx.GetRealPath(userscope, path)
if err != nil {
return http.StatusNotFound, fmt.Errorf("file not found: %v", err)
}
metadata, exists := idx.GetMetadataInfo(userscope, true)
if !exists {
return http.StatusNotFound, fmt.Errorf("file not found: %v", err)
}
index, err := strconv.Atoi(indexParam)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid index parameter: %v", err)
}
parentDir := filepath.Dir(realPath)
subtitle, err := ffmpeg.ExtractSingleSubtitle(realPath, parentDir, index, metadata.ModTime)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to extract subtitle: %v", err)
}
w.Header().Set("Content-Type", "text/vtt; charset=utf-8")
w.Header().Set("Content-Disposition", "inline")
w.Header().Set("Cache-Control", "private")
http.ServeContent(w, r, fmt.Sprintf("%s-%d.vtt", subtitle.Name, index),
time.Now(), bytes.NewReader([]byte(subtitle.Content)))
return http.StatusOK, nil
}
+10 -3
View File
@@ -15,6 +15,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
"github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
"github.com/gtsteffaniak/filebrowser/backend/database/share"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
@@ -90,11 +91,16 @@ func withHashFileHelper(fn handleFunc) handleFunc {
Expand: true,
Content: getContent,
})
file.Token = link.Token
if err != nil {
logger.Errorf("error fetching file info for share. hash=%v path=%v error=%v", hash, path, err)
return errToStatus(err), fmt.Errorf("error fetching share from server")
}
file.Token = link.Token
file.Source = ""
file.Hash = link.Hash
if !link.EnableOnlyOffice || !link.DisableFileViewer {
file.OnlyOfficeId = ""
}
if getContent && file.Content != "" {
link.Mu.Lock()
link.Downloads++
@@ -102,7 +108,7 @@ func withHashFileHelper(fn handleFunc) handleFunc {
}
file.Path = "/" + strings.TrimPrefix(strings.TrimPrefix(file.Path, link.Path), "/")
// Set the file info in the `data` object
data.fileInfo = file
data.fileInfo = *file
// Call the next handler with the data
return fn(w, r, data)
})
@@ -180,7 +186,8 @@ func withOrWithoutUserHelper(fn handleFunc) handleFunc {
}
// Only fall back to anonymous if authentication actually failed
if status == http.StatusUnauthorized || status == http.StatusForbidden {
data.user = &users.AnonymousUser
data.user = &users.User{Username: "anonymous"}
settings.ApplyUserDefaults(data.user)
// If user authentication failed, call the handler without user context
// Clear any user data that might have been partially set
data.token = ""
+2 -2
View File
@@ -43,8 +43,8 @@ func mockFileInfoFaster(t *testing.T) {
t.Cleanup(func() { FileInfoFasterFunc = originalFileInfoFaster })
// Mock the function to skip execution
FileInfoFasterFunc = func(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error) {
return iteminfo.ExtendedFileInfo{
FileInfoFasterFunc = func(opts iteminfo.FileOptions) (*iteminfo.ExtendedFileInfo, error) {
return &iteminfo.ExtendedFileInfo{
FileInfo: iteminfo.FileInfo{
Path: opts.Path,
ItemInfo: iteminfo.ItemInfo{
+230 -105
View File
@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
@@ -38,68 +39,107 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
if settings.Config.Integrations.OnlyOffice.Url == "" {
return http.StatusInternalServerError, errors.New("only-office integration must be configured in settings")
}
encodedUrl := r.URL.Query().Get("url")
// Decode the URL-encoded path
givenUrl, err := url.QueryUnescape(encodedUrl)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
// Extract clean parameters from request
source := r.URL.Query().Get("source")
path := r.URL.Query().Get("path")
// Validate required parameters
if (path == "" || source == "") && d.fileInfo.Hash == "" {
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
}
// get path from url
pathParts := strings.Split(givenUrl, "/api/raw?files=")
origPathParts := strings.Split(encodedUrl, "/api/raw?files=")
encodedPath := origPathParts[len(origPathParts)-1]
sourceFile := pathParts[len(pathParts)-1]
sourceSplit := strings.Split(sourceFile, "::")
if len(sourceSplit) != 2 {
return http.StatusBadRequest, fmt.Errorf("invalid url path %v", givenUrl)
themeMode := utils.Ternary(d.user.DarkMode, "dark", "light")
var sourceInfo settings.Source
var ok bool
if d.fileInfo.Hash != "" {
sourceInfo, ok = settings.Config.Server.SourceMap[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
} else {
sourceInfo, ok = settings.Config.Server.NameToSource[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
}
source := sourceSplit[0]
path := sourceSplit[1]
urlFirst := pathParts[0]
if settings.Config.Server.InternalUrl != "" {
urlFirst = settings.Config.Server.InternalUrl + settings.Config.Server.BaseURL
whatToReplace := strings.Split(givenUrl, "/api/raw")[0] + "/"
givenUrl = strings.Replace(givenUrl, whatToReplace, urlFirst, 1)
}
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
if err != nil {
return http.StatusForbidden, err
}
fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{
Path: utils.JoinPathAsUnix(userscope, path),
Modify: d.user.Permissions.Modify,
Source: source,
Expand: false,
})
if err != nil {
logger.Debugf("onlyofficeClientConfigGetHandler: failed to get file info for file source %v, path %v: %v", source, path, err)
return errToStatus(err), err
}
id, err := getOnlyOfficeId(source, fileInfo.Path)
if err != nil {
logger.Debugf("getOnlyOfficeId failed for file source %v, path %v: %v", source, fileInfo.Path, err)
return http.StatusNotFound, err
}
split := strings.Split(fileInfo.Name, ".")
fileType := split[len(split)-1]
theme := "light"
if d.user.DarkMode {
theme = "dark"
if d.fileInfo.Hash == "" {
// Build file info based on whether this is a share or regular request
// Regular user request - need to resolve scope
userScope, scopeErr := settings.GetScopeFromSourceName(d.user.Scopes, source)
if scopeErr != nil {
logger.Errorf("OnlyOffice: source %s not available for user %s: %v", source, d.user.Username, scopeErr)
return http.StatusForbidden, fmt.Errorf("source %s is not available", source)
}
indexPath := utils.JoinPathAsUnix(userScope, path)
logger.Debugf("OnlyOffice user request: resolved path=%s", indexPath)
fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{
Path: indexPath,
Modify: d.user.Permissions.Modify,
Source: source,
Expand: false,
})
if err != nil {
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, indexPath, err)
return errToStatus(err), err
}
d.fileInfo = *fileInfo
} else {
source = sourceInfo.Name
// path is index path, so we build from share path
path = utils.JoinPathAsUnix(d.share.Path, path)
if d.share.EnforceDarkLightMode == "dark" {
themeMode = "dark"
}
if d.share.EnforceDarkLightMode == "light" {
themeMode = "light"
}
}
// Determine file type and editing permissions
fileType := strings.TrimPrefix(filepath.Ext(d.fileInfo.Name), ".")
canEdit := iteminfo.CanEditOnlyOffice(d.user.Permissions.Modify, fileType)
mode := "view"
if canEdit {
mode = "edit"
canEditMode := utils.Ternary(canEdit, "edit", "view")
if d.fileInfo.Hash != "" {
if d.share.EnableOnlyOfficeEditing {
canEditMode = "edit"
}
}
callbackURL := fmt.Sprintf("%v/api/onlyoffice/callback?files=%v&auth=%v", strings.TrimSuffix(urlFirst, "/"), encodedPath, d.token)
// For shares, we need to keep track of the original relative path for the callback URL
var callbackPath string
if d.fileInfo.Hash != "" {
// For shares, use the original path parameter (relative to share)
callbackPath = r.URL.Query().Get("path")
} else {
// For regular requests, use the processed path
callbackPath = path
}
// Generate document ID for OnlyOffice
documentId, err := getOnlyOfficeId(d.fileInfo.RealPath)
if err != nil {
logger.Errorf("OnlyOffice: failed to generate document ID for source=%s, path=%s: %v", source, path, err)
return http.StatusNotFound, fmt.Errorf("failed to generate document ID: %v", err)
}
// Build download URL that OnlyOffice server will use
downloadURL := buildOnlyOfficeDownloadURL(source, callbackPath, d.fileInfo.Hash, d.token)
// Build callback URL for OnlyOffice to notify us of changes
callbackURL := buildOnlyOfficeCallbackURL(source, callbackPath, d.fileInfo.Hash, d.token)
// Build OnlyOffice client configuration
clientConfig := map[string]interface{}{
"document": map[string]interface{}{
"fileType": fileType,
"key": id,
"title": fileInfo.Name,
"url": givenUrl + "&auth=" + d.token,
"key": documentId,
"title": d.fileInfo.Name,
"url": downloadURL,
"permissions": map[string]interface{}{
"edit": canEdit,
"edit": canEditMode,
"download": true,
"print": true,
},
@@ -113,90 +153,201 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
"customization": map[string]interface{}{
"autosave": true,
"forcesave": true,
"uiTheme": theme,
"uiTheme": themeMode,
},
"lang": d.user.Locale,
"mode": mode,
"mode": canEditMode,
},
}
// Sign configuration with JWT if secret is configured
if settings.Config.Integrations.OnlyOffice.Secret != "" {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(clientConfig))
signature, err := token.SignedString([]byte(settings.Config.Integrations.OnlyOffice.Secret))
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to sign JWT")
logger.Errorf("OnlyOffice: failed to sign JWT: %v", err)
return http.StatusInternalServerError, fmt.Errorf("failed to sign configuration")
}
clientConfig["token"] = signature
}
return renderJSON(w, r, clientConfig)
}
// buildOnlyOfficeDownloadURL constructs the download URL that OnlyOffice server will use to fetch the file
func buildOnlyOfficeDownloadURL(source, path, hash, token string) string {
// Determine base URL (internal URL takes priority for OnlyOffice server communication)
baseURL := settings.Config.Server.BaseURL
if settings.Config.Server.InternalUrl != "" {
baseURL = settings.Config.Server.InternalUrl + settings.Config.Server.BaseURL
}
var downloadURL string
if hash != "" {
// Share download URL - don't expose source name, just use the path relative to share
filesParam := url.QueryEscape(path)
downloadURL = fmt.Sprintf("%s/public/api/raw?files=%s&hash=%s&token=%s&auth=%s",
strings.TrimSuffix(baseURL, "/"), filesParam, hash, token, token)
} else {
// Regular download URL - include source for non-share requests
filesParam := url.QueryEscape(source + "::" + path)
downloadURL = fmt.Sprintf("%s/api/raw?files=%s&auth=%s",
strings.TrimSuffix(baseURL, "/"), filesParam, token)
}
return downloadURL
}
// buildOnlyOfficeCallbackURL constructs the callback URL that OnlyOffice server will use to notify us of changes
func buildOnlyOfficeCallbackURL(source, path, hash, token string) string {
baseURL := settings.Config.Server.BaseURL
if settings.Config.Server.InternalUrl != "" {
baseURL = settings.Config.Server.InternalUrl + settings.Config.Server.BaseURL
}
var callbackURL string
if hash != "" {
// Share callback URL - use public API and don't expose source, use path relative to share
params := url.Values{}
params.Set("hash", hash)
params.Set("path", path) // This should be the path relative to the share, not the full filesystem path
params.Set("auth", token)
callbackURL = fmt.Sprintf("%s/public/api/onlyoffice/callback?%s",
strings.TrimSuffix(baseURL, "/"), params.Encode())
} else {
// Regular callback URL - include source for non-share requests
params := url.Values{}
params.Set("source", source)
params.Set("path", path)
params.Set("auth", token)
callbackURL = fmt.Sprintf("%s/api/onlyoffice/callback?%s",
strings.TrimSuffix(baseURL, "/"), params.Encode())
}
return callbackURL
}
func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// Parse OnlyOffice callback data
body, err := io.ReadAll(r.Body)
if err != nil {
return http.StatusInternalServerError, err
}
var data OnlyOfficeCallback
err = json.Unmarshal(body, &data)
if err != nil {
logger.Errorf("OnlyOffice callback: failed to read request body: %v", err)
return http.StatusInternalServerError, err
}
encodedPath := r.URL.Query().Get("files")
pathParts := strings.Split(encodedPath, "::")
if len(pathParts) < 2 {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
source := pathParts[0]
// Decode the URL-encoded path
path, err := url.QueryUnescape(pathParts[1])
var data OnlyOfficeCallback
err = json.Unmarshal(body, &data)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
logger.Errorf("OnlyOffice callback: failed to parse JSON: %v", err)
return http.StatusInternalServerError, err
}
// Extract clean parameters from query string
source := r.URL.Query().Get("source")
path := r.URL.Query().Get("path")
// Validate required parameters
if (path == "" || source == "") && d.fileInfo.Hash == "" {
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
}
var sourceInfo settings.Source
var ok bool
if d.fileInfo.Hash != "" {
sourceInfo, ok = settings.Config.Server.SourceMap[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
} else {
sourceInfo, ok = settings.Config.Server.NameToSource[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
}
}
if d.fileInfo.Hash == "" {
// Regular user request - need to resolve scope
userScope, scopeErr := settings.GetScopeFromSourceName(d.user.Scopes, source)
if scopeErr != nil {
logger.Errorf("OnlyOffice callback: source %s not available for user %s: %v", source, d.user.Username, scopeErr)
return http.StatusForbidden, fmt.Errorf("source %s is not available", source)
}
path = utils.JoinPathAsUnix(userScope, path)
} else {
source = sourceInfo.Name
// path is index path, so we build from share path
path = utils.JoinPathAsUnix(d.share.Path, path)
}
// Handle document closure - clean up document key cache
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
data.Status == onlyOfficeStatusDocumentClosedWithNoChanges {
// Refer to only-office documentation
// Refer to OnlyOffice documentation:
// - https://api.onlyoffice.com/editors/coedit
// - https://api.onlyoffice.com/editors/callback
//
// When the document is fully closed by all editors,
// then the document key should no longer be re-used.
// the document key should no longer be re-used.
logger.Debugf("OnlyOffice: document closed, cleaning up document ID for source=%s, path=%s", source, path)
deleteOfficeId(source, path)
}
// Handle document save operations
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
data.Status == onlyOfficeStatusForceSaveWhileDocumentStillOpen {
// Verify user has modify permissions
if !d.user.Permissions.Modify {
logger.Warningf("OnlyOffice callback: user %s lacks modify permissions for source=%s, path=%s",
d.user.Username, source, path)
return http.StatusForbidden, nil
}
// Download the updated document from OnlyOffice server
doc, err := http.Get(data.URL)
if err != nil {
logger.Errorf("OnlyOffice callback: failed to download updated document: %v", err)
return http.StatusInternalServerError, err
}
defer doc.Body.Close()
// Resolve file path for writing (same logic as in config handler)
var resolvedPath string
if d.fileInfo.Hash == "" {
// Regular user request - need to resolve scope
userScope, scopeErr := settings.GetScopeFromSourceName(d.user.Scopes, source)
if scopeErr != nil {
logger.Errorf("OnlyOffice callback: source %s not available for user %s: %v",
source, d.user.Username, scopeErr)
return http.StatusForbidden, fmt.Errorf("source %s is not available", source)
}
resolvedPath = utils.JoinPathAsUnix(userScope, path)
}
// Write the updated document
fileOpts := iteminfo.FileOptions{
Path: path,
Path: resolvedPath,
Source: source,
}
writeErr := files.WriteFile(fileOpts, doc.Body)
if writeErr != nil {
logger.Errorf("OnlyOffice callback: failed to write updated document: %v", writeErr)
return http.StatusInternalServerError, writeErr
}
}
// Return success response to OnlyOffice server
resp := map[string]int{
"error": 0,
}
return renderJSON(w, r, resp)
}
func getOnlyOfficeId(source, path string) (string, error) {
idx := indexing.GetIndex(source)
if idx == nil {
return "", fmt.Errorf("source not found")
}
realpath, _, _ := idx.GetRealPath(path)
func getOnlyOfficeId(realpath string) (string, error) {
// error is intentionally ignored in order treat errors
// the same as a cache-miss
cachedDocumentKey, ok := utils.OnlyOfficeCache.Get(realpath).(string)
@@ -215,29 +366,3 @@ func deleteOfficeId(source, path string) {
realpath, _, _ := idx.GetRealPath(path)
utils.OnlyOfficeCache.Delete(realpath)
}
func onlyofficeGetTokenHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// get config from body
body, err := io.ReadAll(r.Body)
if err != nil {
return http.StatusInternalServerError, err
}
defer r.Body.Close()
var payload map[string]interface{}
// marshall to struct
err = json.Unmarshal(body, &payload)
if err != nil {
return http.StatusInternalServerError, err
}
if settings.Config.Integrations.OnlyOffice.Secret != "" {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload))
ss, err := token.SignedString([]byte(settings.Config.Integrations.OnlyOffice.Secret))
if err != nil {
return 500, errors.New("could not generate a new jwt")
}
return renderJSON(w, r, map[string]string{"token": ss})
}
return 400, fmt.Errorf("bad request")
}
+1 -1
View File
@@ -70,7 +70,7 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
if err != nil {
return errToStatus(err), err
}
d.fileInfo = fileInfo
d.fileInfo = *fileInfo
return previewHelperFunc(w, r, d)
}
+13 -7
View File
@@ -7,6 +7,7 @@ import (
"net/url"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
"github.com/gtsteffaniak/go-logger/logger"
@@ -37,12 +38,19 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
d.share.Mu.Unlock()
encodedFiles := r.URL.Query().Get("files")
// Decode the URL-encoded path
f, err := url.QueryUnescape(encodedFiles)
// Decode the URL-encoded path - use PathUnescape to preserve + as literal character
f, err := url.PathUnescape(encodedFiles)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
// Get the actual source name from the share's source mapping
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
if !ok {
return http.StatusInternalServerError, fmt.Errorf("source not found for share")
}
actualSourceName := sourceInfo.Name
fileList := []string{}
for _, file := range strings.Split(f, "||") {
// Check if file already contains source prefix (source::path format)
@@ -57,12 +65,12 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
} else {
// Fallback: treat as plain path
filePath := utils.JoinPathAsUnix(d.share.Path, file)
fileList = append(fileList, d.fileInfo.Source+"::"+filePath)
fileList = append(fileList, actualSourceName+"::"+filePath)
}
} else {
// Plain path without source prefix
// Plain path without source prefix - use the actual source name from share
filePath := utils.JoinPathAsUnix(d.share.Path, file)
fileList = append(fileList, d.fileInfo.Source+"::"+filePath)
fileList = append(fileList, actualSourceName+"::"+filePath)
}
}
@@ -76,8 +84,6 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
}
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// disable onlyoffice for public share
d.fileInfo.OnlyOfficeId = ""
return renderJSON(w, r, d.fileInfo)
}
+5 -5
View File
@@ -176,7 +176,7 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
}
// delete thumbnails
preview.DelThumbs(r.Context(), fileInfo)
preview.DelThumbs(r.Context(), *fileInfo)
err = files.DeleteFiles(source, fileInfo.RealPath, filepath.Dir(fileInfo.RealPath))
if err != nil {
@@ -269,7 +269,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
}
// On the first chunk, check for conflicts or handle override
if offset == 0 {
var fileInfo iteminfo.ExtendedFileInfo
var fileInfo *iteminfo.ExtendedFileInfo
fileInfo, err = files.FileInfoFaster(fileOpts)
if err == nil { // File exists
if r.URL.Query().Get("override") != "true" {
@@ -278,7 +278,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
return http.StatusConflict, nil
}
// If overriding, delete existing thumbnails
preview.DelThumbs(r.Context(), fileInfo)
preview.DelThumbs(r.Context(), *fileInfo)
}
}
@@ -341,7 +341,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
return http.StatusConflict, nil
}
preview.DelThumbs(r.Context(), fileInfo)
preview.DelThumbs(r.Context(), *fileInfo)
}
err = files.WriteFile(fileOpts, r.Body)
if err != nil {
@@ -571,7 +571,7 @@ func patchAction(ctx context.Context, params patchActionParams) error {
}
// delete thumbnails
preview.DelThumbs(ctx, fileInfo)
preview.DelThumbs(ctx, *fileInfo)
return files.MoveResource(params.isSrcDir, params.isDstDir, params.srcIndex, params.dstIndex, params.src, params.dst, store.Share)
default:
return fmt.Errorf("unsupported action %s: %w", params.action, errors.ErrInvalidRequestParams)
+3
View File
@@ -64,6 +64,9 @@ func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
if len(query) < settings.Config.Server.MinSearchLength {
return http.StatusBadRequest, fmt.Errorf("query is too short, minimum length is %d", settings.Config.Server.MinSearchLength)
}
searchScope := strings.TrimPrefix(unencodedScope, ".")
// Retrieve the User-Agent and X-Auth headers from the request
sessionId := r.Header.Get("SessionId")
+35
View File
@@ -2,6 +2,8 @@ package http
import (
"net/http"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
)
// settingsGetHandler retrieves the current system settings.
@@ -34,3 +36,36 @@ func settingsGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
}
return renderJSON(w, r, config)
}
// settingsConfigHandler returns the current system settings as YAML.
// @Summary Get system settings as YAML
// @Description Returns the current running configuration settings as YAML format with optional comments and filtering.
// @Tags Settings
// @Accept json
// @Produce text/plain
// @Param full query boolean false "Show all values (true) or only non-default values (false, default)"
// @Param comments query boolean false "Include comments in YAML (true) or plain YAML (false, default)"
// @Success 200 {string} string "System settings in YAML format"
// @Router /api/settings/config [get]
func settingsConfigHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// Parse query parameters
fullParam := r.URL.Query().Get("full")
commentsParam := r.URL.Query().Get("comments")
showFull := fullParam == "true"
showComments := commentsParam == "true"
// Generate YAML using the existing generator function (don't filter deprecated fields in API)
yamlOutput, err := settings.GenerateConfigYaml(config, showComments, showFull, false)
if err != nil {
return http.StatusInternalServerError, err
}
// Set content type and write response
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
if _, err := w.Write([]byte(yamlOutput)); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
+39 -6
View File
@@ -17,10 +17,19 @@ var templateRenderer *TemplateRenderer
type TemplateRenderer struct {
templates *template.Template
devMode bool
}
// 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.
if t.devMode {
var err error
t.templates, err = t.templates.ParseFS(assetFs, "public/index.html")
if err != nil {
return fmt.Errorf("error reloading template: %w", err)
}
}
// Set headers
w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
w.Header().Set("Pragma", "no-cache")
@@ -39,28 +48,37 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
}
userSelectedTheme := ""
if d.user != nil {
theme, ok := config.Frontend.Styling.CustomThemes[d.user.CustomTheme]
theme, ok := config.Frontend.Styling.CustomThemeOptions[d.user.CustomTheme]
if ok {
userSelectedTheme = theme.CSS
userSelectedTheme = theme.CssRaw
}
}
defaultThemeColor := "#455a64"
staticURL := config.Server.BaseURL + "static"
publicStaticURL := config.Server.BaseURL + "public/static"
// Use custom favicon if configured and validated, otherwise fall back to default
var favicon string
if config.Frontend.Favicon != "" {
favicon = staticURL + "/favicon"
} else {
// Default favicon
favicon = staticURL + "/img/icons/favicon-256x256.png"
}
data := map[string]interface{}{
"title": config.Frontend.Name,
"customCSS": config.Frontend.Styling.CustomCSS,
"customCSS": config.Frontend.Styling.CustomCSSRaw,
"userSelectedTheme": userSelectedTheme,
"lightBackground": config.Frontend.Styling.LightBackground,
"darkBackground": config.Frontend.Styling.DarkBackground,
"staticURL": staticURL,
"baseURL": config.Server.BaseURL,
"favicon": staticURL + "/img/icons/favicon-256x256.png",
"favicon": favicon,
"color": defaultThemeColor,
"winIcon": staticURL + "/img/icons/mstile-144x144.png",
"appIcon": staticURL + "/img/icons/android-chrome-256x256.png",
"description": "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server",
"description": config.Frontend.Description,
}
shareProps := map[string]interface{}{
"isShare": false,
@@ -98,6 +116,9 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
shareProps["disableSidebar"] = d.share.DisableSidebar
shareProps["isPasswordProtected"] = d.share.PasswordHash != ""
shareProps["downloadURL"] = getDownloadURL(r, d.share.Hash)
shareProps["enforceDarkLightMode"] = d.share.EnforceDarkLightMode
shareProps["enableOnlyOffice"] = d.share.EnableOnlyOffice
shareProps["enableOnlyOfficeEditing"] = d.share.EnableOnlyOfficeEditing
if d.share.Favicon != "" {
if strings.HasPrefix(d.share.Favicon, "http") {
data["favicon"] = d.share.Favicon
@@ -115,13 +136,19 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
// base url could be different for routes behind proxy
data["staticURL"] = publicStaticURL
data["favicon"] = publicStaticURL + "/img/icons/favicon-256x256.png"
// Use custom favicon for shares too if configured
if config.Frontend.Favicon != "" {
data["favicon"] = publicStaticURL + "/favicon"
} else {
data["favicon"] = publicStaticURL + "/img/icons/favicon-256x256.png"
}
data["winIcon"] = publicStaticURL + "/img/icons/mstile-144x144.png"
data["appIcon"] = publicStaticURL + "/img/icons/android-chrome-256x256.png"
}
// variables consumed by frontend as json
data["globalVars"] = map[string]interface{}{
"name": config.Frontend.Name,
"minSearchLength": config.Server.MinSearchLength,
"disableExternal": config.Frontend.DisableDefaultLinks,
"darkMode": settings.Config.UserDefaults.DarkMode,
"baseURL": config.Server.BaseURL,
@@ -162,6 +189,12 @@ func staticFilesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%v", maxAge))
w.Header().Set("Content-Security-Policy", `default-src 'self'; style-src 'unsafe-inline';`)
// Handle custom favicon if configured and requested
if r.URL.Path == "favicon" && config.Frontend.Favicon != "" {
http.ServeFile(w, r, config.Frontend.Favicon)
return
}
adjustedCompressed := r.URL.Path + ".gz"
if strings.HasSuffix(r.URL.Path, ".js") {
w.Header().Set("Content-Type", "application/javascript; charset=utf-8") // Set the correct MIME type for JavaScript files
+10 -2
View File
@@ -270,11 +270,16 @@ func (i *ItemInfo) DetectType(realPath string, saveContent bool) {
ext := filepath.Ext(name)
// Attempt MIME detection by file extension
if ext == ".md" {
switch ext {
case ".md":
i.Type = "text/markdown"
return
case ".heic", ".heif":
i.Type = "image/heic"
return
}
i.Type = strings.Split(mime.TypeByExtension(ext), ";")[0]
if i.Type == "" {
i.Type = ExtendedMimeTypeCheck(ext)
}
@@ -285,10 +290,13 @@ func (i *ItemInfo) DetectType(realPath string, saveContent bool) {
i.Type = DetectTypeByHeader(realPath)
return
}
if i.Type == "blob" {
if i.Type == "blob" || i.Type == "" {
i.Type = DetectTypeByHeader(realPath)
}
}
if i.Type == "" || i.Type == "application/octet-stream" {
i.Type = "blob"
}
}
// DetectTypeByHeader detects the MIME type of a file based on its header.
+9 -7
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/gtsteffaniak/filebrowser/backend/database/access"
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
)
type ItemInfo struct {
@@ -28,13 +29,14 @@ type FileInfo struct {
// extra calculated fields can be added here
type ExtendedFileInfo struct {
FileInfo
Content string `json:"content,omitempty"` // text content of a file, if requested
Subtitles []string `json:"subtitles,omitempty"` // subtitles for video files
Checksums map[string]string `json:"checksums,omitempty"` // checksums for the file
Token string `json:"token,omitempty"` // token for the file -- used for sharing
OnlyOfficeId string `json:"onlyOfficeId,omitempty"` // id for onlyoffice files
Source string `json:"source"` // associated index source for the file
RealPath string `json:"-"`
Content string `json:"content,omitempty"` // text content of a file, if requested
Subtitles []ffmpeg.SubtitleTrack `json:"subtitles,omitempty"` // subtitles for video files
Checksums map[string]string `json:"checksums,omitempty"` // checksums for the file
Token string `json:"token,omitempty"` // token for the file -- used for sharing
OnlyOfficeId string `json:"onlyOfficeId,omitempty"` // id for onlyoffice files
Source string `json:"source,omitempty"` // associated index source for the file
Hash string `json:"hash,omitempty"` // hash for the file -- used for sharing
RealPath string `json:"-"`
}
// FileOptions are the options when getting a file info.
+27 -13
View File
@@ -8,29 +8,43 @@ import (
"strconv"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
"github.com/gtsteffaniak/go-logger/logger"
)
type SubtitleTrack struct {
Name string `json:"name"` // filename for external, or descriptive name for embedded
Language string `json:"language,omitempty"` // language code
Title string `json:"title,omitempty"` // title/description
Index *int `json:"index,omitempty"` // stream index for embedded subtitles (nil for external)
Codec string `json:"codec,omitempty"` // codec name for embedded subtitles
IsFile bool `json:"isFile"` // true for external files, false for embedded
}
type FFProbeOutput struct {
Streams []struct {
Index int `json:"index"`
CodecType string `json:"codec_type"`
CodecName string `json:"codec_name"`
Tags map[string]string `json:"tags,omitempty"`
Disposition map[string]int `json:"disposition,omitempty"`
} `json:"streams"`
}
// detects subtitles for video files.
func (i *ExtendedFileInfo) DetectSubtitles(parentInfo *FileInfo) {
if !strings.HasPrefix(i.Type, "video") {
logger.Debug("subtitles are not supported for this file : " + i.Name)
return
}
ext := filepath.Ext(i.Name)
baseWithoutExt := strings.TrimSuffix(i.Name, ext)
for _, f := range parentInfo.Files {
baseName := strings.TrimSuffix(i.Name, ext)
if baseName != baseWithoutExt {
continue
}
// Use unified subtitle detection that finds both embedded and external
parentDir := filepath.Dir(i.RealPath)
i.Subtitles = ffmpeg.DetectAllSubtitles(i.RealPath, parentDir, i.ModTime)
}
for _, subtitleExt := range SubtitleExts {
if strings.HasSuffix(f.Name, subtitleExt) {
i.Subtitles = append(i.Subtitles, f.Name)
}
}
}
// LoadSubtitleContent loads the actual content for all detected subtitle tracks
func (i *ExtendedFileInfo) LoadSubtitleContent() error {
return ffmpeg.LoadAllSubtitleContent(i.RealPath, i.Subtitles, i.ModTime)
}
func (info *FileInfo) SortItems() {
+29
View File
@@ -0,0 +1,29 @@
package preview
import (
"path/filepath"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
"github.com/gtsteffaniak/go-logger/logger"
)
// isHEICFile checks if the file is a HEIC file based on MIME type or extension
func isHEICFile(file iteminfo.ExtendedFileInfo) bool {
// Check by MIME type first
if file.Type == "image/heic" {
return true
}
// Check by file extension (in case MIME type detection fails)
ext := strings.ToLower(filepath.Ext(file.Name))
return ext == ".heic" || ext == ".heif"
}
// processHEICFile handles HEIC file processing by delegating to FFmpeg conversion
func (s *Service) processHEICFile(file iteminfo.ExtendedFileInfo, previewSize string) ([]byte, error) {
logger.Infof("🔄 HEIC: Processing HEIC file %s (size: %s)", filepath.Base(file.Name), previewSize)
// Delegate to FFmpeg conversion logic
return s.convertHEICToJPEGWithFFmpeg(file.RealPath, previewSize)
}
+66
View File
@@ -0,0 +1,66 @@
package preview
import (
"context"
"fmt"
"path/filepath"
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
"github.com/gtsteffaniak/go-logger/logger"
)
// convertHEICToJPEGWithFFmpeg converts a HEIC file to JPEG format using FFmpeg
// This function handles all FFmpeg-related logic and parameters
func (s *Service) convertHEICToJPEGWithFFmpeg(filePath string, previewSize string) ([]byte, error) {
logger.Infof("🎯 HEIC FFMPEG: Starting conversion for %s (size: %s)", filepath.Base(filePath), previewSize)
if s.ffmpegPath == "" {
logger.Errorf("❌ HEIC FFMPEG: FFmpeg path is empty")
return nil, fmt.Errorf("FFmpeg is not available for HEIC conversion")
}
logger.Infof("🔧 HEIC FFMPEG: Using FFmpeg at: %s", s.ffmpegPath)
if err := s.acquire(context.Background()); err != nil {
logger.Errorf("❌ HEIC FFMPEG: Failed to acquire semaphore: %v", err)
return nil, err
}
defer s.release()
// Create FFmpeg image service
imageService := ffmpeg.NewImageService(s.ffmpegPath, s.ffprobePath, s.debug)
// Determine target dimensions and quality based on preview size
var width, height int
var quality string
switch previewSize {
case "large":
width, height = 640, 640
quality = "2" // High quality for FFmpeg -q:v
logger.Infof("📐 HEIC FFMPEG: Using LARGE size - target: %dx%d, quality: %s", width, height, quality)
case "small":
width, height = 256, 256
quality = "5" // Medium quality
logger.Infof("📐 HEIC FFMPEG: Using SMALL size - target: %dx%d, quality: %s", width, height, quality)
case "original":
// For original size - no scaling, maximum quality
logger.Infof("📏 HEIC FFMPEG: Using ORIGINAL size - no scaling, maximum quality")
width, height = 0, 0 // Signal to not apply scaling
quality = "1" // Maximum quality for original
logger.Infof("📐 HEIC FFMPEG: Using ORIGINAL mode - no scaling, quality: %s", quality)
default:
logger.Errorf("❌ HEIC FFMPEG: Unsupported preview size: %s", previewSize)
return nil, ErrUnsupportedFormat
}
logger.Infof("🚀 HEIC FFMPEG: Starting FFmpeg conversion with dimensions %dx%d, quality %s", width, height, quality)
result, err := imageService.ConvertHEICToJPEG(filePath, width, height, quality)
if err != nil {
logger.Errorf("❌ HEIC FFMPEG: Conversion failed: %v", err)
return nil, err
}
logger.Infof("✅ HEIC FFMPEG: Conversion successful, output size: %d bytes", len(result))
return result, nil
}
+23 -4
View File
@@ -1,4 +1,3 @@
//go:generate go-enum --sql --marshal --file $GOFILE
package preview
import (
@@ -8,9 +7,9 @@ import (
"image"
"io"
"github.com/kovidgoyal/imaging"
exif "github.com/dsoprea/go-exif/v3"
exifcommon "github.com/dsoprea/go-exif/v3/common"
"github.com/kovidgoyal/imaging"
)
// Format is an image file format.
@@ -21,6 +20,7 @@ png
gif
tiff
bmp
heic
)
*/
type Format int
@@ -37,6 +37,8 @@ func (x Format) toImaging() imaging.Format {
return imaging.TIFF
case FormatBmp:
return imaging.BMP
case FormatHeic:
return imaging.JPEG
default:
return imaging.JPEG
}
@@ -73,6 +75,12 @@ fill
type ResizeMode int
func (s *Service) FormatFromExtension(ext string) (Format, error) {
// heic is not supported by imaging, so we return FormatHeic
switch ext {
case ".heic", ".heif":
return FormatHeic, nil
}
format, err := imaging.FormatFromExtension(ext)
if err != nil {
return -1, ErrUnsupportedFormat
@@ -139,6 +147,10 @@ func (s *Service) Resize(in io.Reader, width, height int, out io.Writer, options
option(&config)
}
if format == FormatHeic {
config.format = FormatJpeg
}
if config.quality == QualityLow && format == FormatJpeg {
thm, newWrappedReader, errThm := getEmbeddedThumbnail(wrappedReader)
wrappedReader = newWrappedReader
@@ -150,16 +162,19 @@ func (s *Service) Resize(in io.Reader, width, height int, out io.Writer, options
}
}
img, err := imaging.Decode(wrappedReader, imaging.AutoOrientation(true))
// For HEIC files, try without AutoOrientation first since it might not work properly
img, err := imaging.Decode(wrappedReader)
if err != nil {
return err
}
// Note: For HEIC files processed via FFmpeg, orientation is handled automatically
switch config.resizeMode {
case ResizeModeFill:
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
case ResizeModeFit:
fallthrough
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
default:
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
}
@@ -176,6 +191,10 @@ func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) {
return 0, nil, fmt.Errorf("%s: %w", err.Error(), ErrUnsupportedFormat)
}
if imgFormat == "heif" {
imgFormat = "heic"
}
format, err := ParseFormat(imgFormat)
if err != nil {
return 0, nil, ErrUnsupportedFormat
+15 -11
View File
@@ -19,16 +19,19 @@ const (
FormatTiff
// FormatBmp is a Format of type Bmp
FormatBmp
// FormatHeic is a Format of type Heic
FormatHeic
)
const _FormatName = "jpegpnggiftiffbmp"
const _FormatName = "jpegpnggiftiffbmpheic"
var _FormatMap = map[Format]string{
0: _FormatName[0:4],
1: _FormatName[4:7],
2: _FormatName[7:10],
3: _FormatName[10:14],
4: _FormatName[14:17],
0: _FormatName[0:4], // jpeg
1: _FormatName[4:7], // png
2: _FormatName[7:10], // gif
3: _FormatName[10:14], // tiff
4: _FormatName[14:17], // bmp
5: _FormatName[17:21], // heic
}
// String implements the Stringer interface.
@@ -40,11 +43,12 @@ func (x Format) String() string {
}
var _FormatValue = map[string]Format{
_FormatName[0:4]: 0,
_FormatName[4:7]: 1,
_FormatName[7:10]: 2,
_FormatName[10:14]: 3,
_FormatName[14:17]: 4,
_FormatName[0:4]: 0, // jpeg
_FormatName[4:7]: 1, // png
_FormatName[7:10]: 2, // gif
_FormatName[10:14]: 3, // tiff
_FormatName[14:17]: 4, // bmp
_FormatName[17:21]: 5, // heic
}
// ParseFormat attempts to convert a string to a Format
+32 -6
View File
@@ -70,11 +70,11 @@ func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir strin
logger.Fatalf("the configured ffmpeg path is not a valid ffprobe binary %s, err: %v", ffmpegPath, err)
}
if errprobe == nil && err == nil {
logger.Infof("Media Enabled : %v", errprobe == nil)
logger.Debugf("Media Enabled : %v", errprobe == nil)
settings.Config.Integrations.Media.FfmpegPath = filepath.Base(ffmpegMainPath)
}
settings.Config.Server.MuPdfAvailable = docEnabled()
logger.Infof("MuPDF Enabled : %v", settings.Config.Server.MuPdfAvailable)
logger.Debugf("MuPDF Enabled : %v", settings.Config.Server.MuPdfAvailable)
return &Service{
sem: make(chan struct{}, concurrencyLimit),
ffmpegPath: ffmpegMainPath,
@@ -126,10 +126,34 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
return nil, fmt.Errorf("failed to create image for office file: %w", err)
}
} else if strings.HasPrefix(file.Type, "image") {
// get image bytes from file
imageBytes, err = os.ReadFile(file.RealPath)
if err != nil {
return nil, fmt.Errorf("failed to read image file: %w", err)
logger.Infof("🔍 PREVIEW DEBUG: Image file detected - Name: %s, Type: %s, Extension: %s", file.Name, file.Type, filepath.Ext(file.Name))
// Quick HEIC identification and delegation
if isHEICFile(file) {
logger.Infof("🔄 HEIC DEBUG: HEIC file detected for %s", filepath.Base(file.Name))
// HEIC files need FFmpeg conversion to JPEG with proper size/quality handling
imageBytes, err = service.processHEICFile(file, previewSize)
if err != nil {
logger.Errorf("❌ HEIC DEBUG: HEIC processing failed: %v", err)
return nil, fmt.Errorf("failed to process HEIC image file: %w", err)
}
logger.Infof("✅ HEIC DEBUG: HEIC processing successful, got %d bytes", len(imageBytes))
// For HEIC files, we've already done the resize/conversion, so cache and return directly
cacheKey := CacheKey(file.RealPath, previewSize, file.ModTime, seekPercentage)
if err = service.fileCache.Store(context.Background(), cacheKey, imageBytes); err != nil {
logger.Errorf("failed to cache HEIC image: %v", err)
}
return imageBytes, nil
} else {
// get image bytes from file (non-HEIC images)
logger.Infof("📖 PREVIEW DEBUG: Reading non-HEIC image file directly")
imageBytes, err = os.ReadFile(file.RealPath)
if err != nil {
return nil, fmt.Errorf("failed to read image file: %w", err)
}
}
} else if strings.HasPrefix(file.Type, "video") {
if seekPercentage == 0 {
@@ -223,6 +247,8 @@ func AvailablePreview(file iteminfo.ExtendedFileInfo) bool {
switch ext {
case ".jpg", ".jpeg", ".png", ".bmp", ".tiff":
return true
case ".heic", ".heif":
return service.ffmpegPath != "" // HEIC support available when FFmpeg is available
}
return file.OnlyOfficeId != ""
}
+4 -71
View File
@@ -1,15 +1,9 @@
package preview
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/gtsteffaniak/go-logger/logger"
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
)
// GenerateVideoPreview generates a single preview image from a video using ffmpeg.
@@ -21,69 +15,8 @@ func (s *Service) GenerateVideoPreview(videoPath, outputPath string, percentageS
return nil, err
}
defer s.release()
// Step 1: Get video duration from the container format
probeCmd := exec.Command(
s.ffprobePath,
"-v", "error",
// Use format=duration for better compatibility
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
videoPath,
)
var probeOut bytes.Buffer
probeCmd.Stdout = &probeOut
if s.debug {
probeCmd.Stderr = os.Stderr
}
if err := probeCmd.Run(); err != nil {
logger.Errorf("ffprobe command failed on file '%v' : %v", videoPath, err)
return nil, fmt.Errorf("ffprobe failed: %w", err)
}
durationStr := strings.TrimSpace(probeOut.String())
if durationStr == "" || durationStr == "N/A" {
logger.Errorf("could not determine video duration for file '%v' using duration info '%v'", videoPath, durationStr)
return nil, fmt.Errorf("could not determine video duration")
}
durationFloat, err := strconv.ParseFloat(durationStr, 64)
if err != nil {
// The original error you saw would be caught here if "N/A" was still the output
return nil, fmt.Errorf("invalid duration: %v", err)
}
if durationFloat <= 0 {
return nil, fmt.Errorf("video duration must be positive")
}
// The rest of your function remains the same...
// Step 2: Get the duration of the video in whole seconds
duration := int(durationFloat)
// Step 3: Calculate seek time based on percentageSeek (percentage value)
seekSeconds := duration * percentageSeek / 100
// Step 4: Convert seekSeconds to string for ffmpeg command
seekTime := strconv.Itoa(seekSeconds)
// Step 5: Extract frame at seek time
cmd := exec.Command(
s.ffmpegPath,
"-ss", seekTime,
"-i", videoPath,
"-frames:v", "1",
"-q:v", "10",
"-y", // overwrite output
outputPath,
)
if s.debug {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err = cmd.Run()
if err != nil {
return nil, fmt.Errorf("ffmpeg command failed on file '%v' : %w", videoPath, err)
}
return os.ReadFile(outputPath)
// Create a temporary video service for this operation
videoSvc := ffmpeg.NewVideoService(s.ffmpegPath, s.ffprobePath, 1, s.debug)
return videoSvc.GenerateVideoPreview(videoPath, outputPath, percentageSeek)
}
+178 -11
View File
@@ -854,6 +854,87 @@ const docTemplate = `{
}
}
},
"/api/media/subtitles": {
"get": {
"description": "Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content",
"consumes": [
"application/json"
],
"produces": [
"text/vtt"
],
"tags": [
"Subtitles"
],
"summary": "Extract embedded subtitles",
"parameters": [
{
"type": "string",
"description": "Index path to the video file",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Source name for the desired source",
"name": "source",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "Stream index for embedded subtitle extraction, defaults to 0",
"name": "index",
"in": "query"
}
],
"responses": {
"200": {
"description": "Raw WebVTT subtitle content",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Resource not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/preview": {
"get": {
"description": "Returns a preview image based on the requested path and size.",
@@ -1616,6 +1697,43 @@ const docTemplate = `{
}
}
},
"/api/settings/config": {
"get": {
"description": "Returns the current running configuration settings as YAML format with optional comments and filtering.",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"Settings"
],
"summary": "Get system settings as YAML",
"parameters": [
{
"type": "boolean",
"description": "Show all values (true) or only non-default values (false, default)",
"name": "full",
"in": "query"
},
{
"type": "boolean",
"description": "Include comments in YAML (true) or plain YAML (false, default)",
"name": "comments",
"in": "query"
}
],
"responses": {
"200": {
"description": "System settings in YAML format",
"schema": {
"type": "string"
}
}
}
}
},
"/api/share": {
"get": {
"description": "Retrieves all share links associated with a specific resource path for the current user.",
@@ -2486,26 +2604,26 @@ const docTemplate = `{
"type": "object",
"properties": {
"adminPassword": {
"description": "the password of the admin user. If not set, the default is \"admin\".",
"description": "secret: the password of the admin user. If not set, the default is \"admin\".",
"type": "string"
},
"adminUsername": {
"description": "the username of the admin user. If not set, the default is \"admin\".",
"description": "secret: the username of the admin user. If not set, the default is \"admin\".",
"type": "string"
},
"key": {
"description": "the key used to sign the JWT tokens. If not set, a random key will be generated.",
"description": "secret: the key used to sign the JWT tokens. If not set, a random key will be generated.",
"type": "string"
},
"methods": {
"$ref": "#/definitions/settings.LoginMethods"
},
"tokenExpirationHours": {
"description": "the number of hours until the token expires. Default is 2 hours.",
"description": "time in hours each web UI session token is valid for. Default is 2 hours.",
"type": "integer"
},
"totpSecret": {
"description": "secret used to encrypt TOTP secrets",
"description": "secret: secret used to encrypt TOTP secrets",
"type": "string"
}
}
@@ -2602,6 +2720,10 @@ const docTemplate = `{
"settings.Frontend": {
"type": "object",
"properties": {
"description": {
"description": "description that shows up in html head meta description",
"type": "string"
},
"disableDefaultLinks": {
"description": "disable default links in the sidebar",
"type": "boolean"
@@ -2620,6 +2742,10 @@ const docTemplate = `{
"$ref": "#/definitions/settings.ExternalLink"
}
},
"favicon": {
"description": "path to a favicon to use for the frontend",
"type": "string"
},
"name": {
"description": "display name",
"type": "string"
@@ -2723,11 +2849,11 @@ const docTemplate = `{
"type": "string"
},
"clientId": {
"description": "client id of the OIDC application",
"description": "secret: client id of the OIDC application",
"type": "string"
},
"clientSecret": {
"description": "client secret of the OIDC application",
"description": "secret: client secret of the OIDC application",
"type": "string"
},
"createUser": {
@@ -2776,6 +2902,7 @@ const docTemplate = `{
"type": "string"
},
"secret": {
"description": "secret: authentication key for OnlyOffice integration",
"type": "string"
},
"url": {
@@ -2885,6 +3012,10 @@ const docTemplate = `{
"description": "disable type detection by header, useful if filesystem is slow.",
"type": "boolean"
},
"disableUpdateCheck": {
"description": "disables backend update check service",
"type": "boolean"
},
"externalUrl": {
"description": "used by share links if set (eg. http://mydomain.com)",
"type": "string"
@@ -2903,6 +3034,10 @@ const docTemplate = `{
"description": "max pre-archive combined size of files/folder that are allowed to be archived (in GB)",
"type": "integer"
},
"minSearchLength": {
"description": "minimum length of search query to begin searching (default: 3)",
"type": "integer"
},
"numImageProcessors": {
"description": "number of concurrent image processing jobs used to create previews, default is number of cpu cores available.",
"type": "integer"
@@ -3073,6 +3208,10 @@ const docTemplate = `{
"description": "when false, the date is relative, when true, the date is an exact timestamp",
"type": "boolean"
},
"debugOffice": {
"description": "debug onlyoffice editor",
"type": "boolean"
},
"deleteWithoutConfirming": {
"description": "delete files without confirmation",
"type": "boolean"
@@ -3086,7 +3225,7 @@ const docTemplate = `{
"type": "string"
},
"disablePreviewExt": {
"description": "comma separated list of file extensions to disable preview for",
"description": "space separated list of file extensions to disable preview for",
"type": "string"
},
"disableQuickToggles": {
@@ -3106,7 +3245,7 @@ const docTemplate = `{
"type": "boolean"
},
"disableViewingExt": {
"description": "comma separated list of file extensions to disable viewing for",
"description": "space separated list of file extensions to disable viewing for",
"type": "string"
},
"editorQuickSave": {
@@ -3217,6 +3356,16 @@ const docTemplate = `{
"downloadsLimit": {
"type": "integer"
},
"enableOnlyOffice": {
"type": "boolean"
},
"enableOnlyOfficeEditing": {
"type": "boolean"
},
"enforceDarkLightMode": {
"description": "\"dark\" or \"light\"",
"type": "string"
},
"expires": {
"type": "string"
},
@@ -3308,6 +3457,16 @@ const docTemplate = `{
"downloadsLimit": {
"type": "integer"
},
"enableOnlyOffice": {
"type": "boolean"
},
"enableOnlyOfficeEditing": {
"type": "boolean"
},
"enforceDarkLightMode": {
"description": "\"dark\" or \"light\"",
"type": "string"
},
"expire": {
"type": "integer"
},
@@ -3434,6 +3593,10 @@ const docTemplate = `{
"description": "autoplay media files in preview",
"type": "boolean"
},
"defaultMediaPlayer": {
"description": "disable html5 media player and use the default media player",
"type": "boolean"
},
"disableHideSidebar": {
"description": "disable the hide sidebar preview for previews and editors",
"type": "boolean"
@@ -3507,6 +3670,10 @@ const docTemplate = `{
"description": "when false, the date is relative, when true, the date is an exact timestamp",
"type": "boolean"
},
"debugOffice": {
"description": "debug onlyoffice editor",
"type": "boolean"
},
"deleteWithoutConfirming": {
"description": "delete files without confirmation",
"type": "boolean"
@@ -3520,7 +3687,7 @@ const docTemplate = `{
"type": "string"
},
"disablePreviewExt": {
"description": "comma separated list of file extensions to disable preview for",
"description": "space separated list of file extensions to disable preview for",
"type": "string"
},
"disableQuickToggles": {
@@ -3539,7 +3706,7 @@ const docTemplate = `{
"type": "boolean"
},
"disableViewingExt": {
"description": "comma separated list of file extensions to disable viewing for",
"description": "space separated list of file extensions to disable viewing for",
"type": "string"
},
"editorQuickSave": {
+178 -11
View File
@@ -843,6 +843,87 @@
}
}
},
"/api/media/subtitles": {
"get": {
"description": "Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content",
"consumes": [
"application/json"
],
"produces": [
"text/vtt"
],
"tags": [
"Subtitles"
],
"summary": "Extract embedded subtitles",
"parameters": [
{
"type": "string",
"description": "Index path to the video file",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Source name for the desired source",
"name": "source",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "Stream index for embedded subtitle extraction, defaults to 0",
"name": "index",
"in": "query"
}
],
"responses": {
"200": {
"description": "Raw WebVTT subtitle content",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Resource not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/preview": {
"get": {
"description": "Returns a preview image based on the requested path and size.",
@@ -1605,6 +1686,43 @@
}
}
},
"/api/settings/config": {
"get": {
"description": "Returns the current running configuration settings as YAML format with optional comments and filtering.",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"Settings"
],
"summary": "Get system settings as YAML",
"parameters": [
{
"type": "boolean",
"description": "Show all values (true) or only non-default values (false, default)",
"name": "full",
"in": "query"
},
{
"type": "boolean",
"description": "Include comments in YAML (true) or plain YAML (false, default)",
"name": "comments",
"in": "query"
}
],
"responses": {
"200": {
"description": "System settings in YAML format",
"schema": {
"type": "string"
}
}
}
}
},
"/api/share": {
"get": {
"description": "Retrieves all share links associated with a specific resource path for the current user.",
@@ -2475,26 +2593,26 @@
"type": "object",
"properties": {
"adminPassword": {
"description": "the password of the admin user. If not set, the default is \"admin\".",
"description": "secret: the password of the admin user. If not set, the default is \"admin\".",
"type": "string"
},
"adminUsername": {
"description": "the username of the admin user. If not set, the default is \"admin\".",
"description": "secret: the username of the admin user. If not set, the default is \"admin\".",
"type": "string"
},
"key": {
"description": "the key used to sign the JWT tokens. If not set, a random key will be generated.",
"description": "secret: the key used to sign the JWT tokens. If not set, a random key will be generated.",
"type": "string"
},
"methods": {
"$ref": "#/definitions/settings.LoginMethods"
},
"tokenExpirationHours": {
"description": "the number of hours until the token expires. Default is 2 hours.",
"description": "time in hours each web UI session token is valid for. Default is 2 hours.",
"type": "integer"
},
"totpSecret": {
"description": "secret used to encrypt TOTP secrets",
"description": "secret: secret used to encrypt TOTP secrets",
"type": "string"
}
}
@@ -2591,6 +2709,10 @@
"settings.Frontend": {
"type": "object",
"properties": {
"description": {
"description": "description that shows up in html head meta description",
"type": "string"
},
"disableDefaultLinks": {
"description": "disable default links in the sidebar",
"type": "boolean"
@@ -2609,6 +2731,10 @@
"$ref": "#/definitions/settings.ExternalLink"
}
},
"favicon": {
"description": "path to a favicon to use for the frontend",
"type": "string"
},
"name": {
"description": "display name",
"type": "string"
@@ -2712,11 +2838,11 @@
"type": "string"
},
"clientId": {
"description": "client id of the OIDC application",
"description": "secret: client id of the OIDC application",
"type": "string"
},
"clientSecret": {
"description": "client secret of the OIDC application",
"description": "secret: client secret of the OIDC application",
"type": "string"
},
"createUser": {
@@ -2765,6 +2891,7 @@
"type": "string"
},
"secret": {
"description": "secret: authentication key for OnlyOffice integration",
"type": "string"
},
"url": {
@@ -2874,6 +3001,10 @@
"description": "disable type detection by header, useful if filesystem is slow.",
"type": "boolean"
},
"disableUpdateCheck": {
"description": "disables backend update check service",
"type": "boolean"
},
"externalUrl": {
"description": "used by share links if set (eg. http://mydomain.com)",
"type": "string"
@@ -2892,6 +3023,10 @@
"description": "max pre-archive combined size of files/folder that are allowed to be archived (in GB)",
"type": "integer"
},
"minSearchLength": {
"description": "minimum length of search query to begin searching (default: 3)",
"type": "integer"
},
"numImageProcessors": {
"description": "number of concurrent image processing jobs used to create previews, default is number of cpu cores available.",
"type": "integer"
@@ -3062,6 +3197,10 @@
"description": "when false, the date is relative, when true, the date is an exact timestamp",
"type": "boolean"
},
"debugOffice": {
"description": "debug onlyoffice editor",
"type": "boolean"
},
"deleteWithoutConfirming": {
"description": "delete files without confirmation",
"type": "boolean"
@@ -3075,7 +3214,7 @@
"type": "string"
},
"disablePreviewExt": {
"description": "comma separated list of file extensions to disable preview for",
"description": "space separated list of file extensions to disable preview for",
"type": "string"
},
"disableQuickToggles": {
@@ -3095,7 +3234,7 @@
"type": "boolean"
},
"disableViewingExt": {
"description": "comma separated list of file extensions to disable viewing for",
"description": "space separated list of file extensions to disable viewing for",
"type": "string"
},
"editorQuickSave": {
@@ -3206,6 +3345,16 @@
"downloadsLimit": {
"type": "integer"
},
"enableOnlyOffice": {
"type": "boolean"
},
"enableOnlyOfficeEditing": {
"type": "boolean"
},
"enforceDarkLightMode": {
"description": "\"dark\" or \"light\"",
"type": "string"
},
"expires": {
"type": "string"
},
@@ -3297,6 +3446,16 @@
"downloadsLimit": {
"type": "integer"
},
"enableOnlyOffice": {
"type": "boolean"
},
"enableOnlyOfficeEditing": {
"type": "boolean"
},
"enforceDarkLightMode": {
"description": "\"dark\" or \"light\"",
"type": "string"
},
"expire": {
"type": "integer"
},
@@ -3423,6 +3582,10 @@
"description": "autoplay media files in preview",
"type": "boolean"
},
"defaultMediaPlayer": {
"description": "disable html5 media player and use the default media player",
"type": "boolean"
},
"disableHideSidebar": {
"description": "disable the hide sidebar preview for previews and editors",
"type": "boolean"
@@ -3496,6 +3659,10 @@
"description": "when false, the date is relative, when true, the date is an exact timestamp",
"type": "boolean"
},
"debugOffice": {
"description": "debug onlyoffice editor",
"type": "boolean"
},
"deleteWithoutConfirming": {
"description": "delete files without confirmation",
"type": "boolean"
@@ -3509,7 +3676,7 @@
"type": "string"
},
"disablePreviewExt": {
"description": "comma separated list of file extensions to disable preview for",
"description": "space separated list of file extensions to disable preview for",
"type": "string"
},
"disableQuickToggles": {
@@ -3528,7 +3695,7 @@
"type": "boolean"
},
"disableViewingExt": {
"description": "comma separated list of file extensions to disable viewing for",
"description": "space separated list of file extensions to disable viewing for",
"type": "string"
},
"editorQuickSave": {
+132 -12
View File
@@ -140,22 +140,25 @@ definitions:
settings.Auth:
properties:
adminPassword:
description: the password of the admin user. If not set, the default is "admin".
description: 'secret: the password of the admin user. If not set, the default
is "admin".'
type: string
adminUsername:
description: the username of the admin user. If not set, the default is "admin".
description: 'secret: the username of the admin user. If not set, the default
is "admin".'
type: string
key:
description: the key used to sign the JWT tokens. If not set, a random key
will be generated.
description: 'secret: the key used to sign the JWT tokens. If not set, a random
key will be generated.'
type: string
methods:
$ref: '#/definitions/settings.LoginMethods'
tokenExpirationHours:
description: the number of hours until the token expires. Default is 2 hours.
description: time in hours each web UI session token is valid for. Default
is 2 hours.
type: integer
totpSecret:
description: secret used to encrypt TOTP secrets
description: 'secret: secret used to encrypt TOTP secrets'
type: string
type: object
settings.CustomTheme:
@@ -229,6 +232,9 @@ definitions:
type: object
settings.Frontend:
properties:
description:
description: description that shows up in html head meta description
type: string
disableDefaultLinks:
description: disable default links in the sidebar
type: boolean
@@ -242,6 +248,9 @@ definitions:
items:
$ref: '#/definitions/settings.ExternalLink'
type: array
favicon:
description: path to a favicon to use for the frontend
type: string
name:
description: display name
type: string
@@ -316,10 +325,10 @@ definitions:
description: if set, users in this group will be granted admin privileges.
type: string
clientId:
description: client id of the OIDC application
description: 'secret: client id of the OIDC application'
type: string
clientSecret:
description: client secret of the OIDC application
description: 'secret: client secret of the OIDC application'
type: string
createUser:
description: create user if not exists
@@ -357,6 +366,7 @@ definitions:
to bypass proxy.
type: string
secret:
description: 'secret: authentication key for OnlyOffice integration'
type: string
url:
description: The URL to the OnlyOffice Document Server, needs to be accessible
@@ -440,6 +450,9 @@ definitions:
disableTypeDetectionByHeader:
description: disable type detection by header, useful if filesystem is slow.
type: boolean
disableUpdateCheck:
description: disables backend update check service
type: boolean
externalUrl:
description: used by share links if set (eg. http://mydomain.com)
type: string
@@ -455,6 +468,10 @@ definitions:
description: max pre-archive combined size of files/folder that are allowed
to be archived (in GB)
type: integer
minSearchLength:
description: 'minimum length of search query to begin searching (default:
3)'
type: integer
numImageProcessors:
description: number of concurrent image processing jobs used to create previews,
default is number of cpu cores available.
@@ -585,6 +602,9 @@ definitions:
description: when false, the date is relative, when true, the date is an exact
timestamp
type: boolean
debugOffice:
description: debug onlyoffice editor
type: boolean
deleteWithoutConfirming:
description: delete files without confirmation
type: boolean
@@ -595,7 +615,7 @@ definitions:
description: list of file extensions to disable onlyoffice editor for
type: string
disablePreviewExt:
description: comma separated list of file extensions to disable preview for
description: space separated list of file extensions to disable preview for
type: string
disableQuickToggles:
description: disable the quick toggles in the sidebar
@@ -610,7 +630,7 @@ definitions:
description: disable update notifications banner for admin users
type: boolean
disableViewingExt:
description: comma separated list of file extensions to disable viewing for
description: space separated list of file extensions to disable viewing for
type: string
editorQuickSave:
description: show quick save button in editor
@@ -690,6 +710,13 @@ definitions:
type: string
downloadsLimit:
type: integer
enableOnlyOffice:
type: boolean
enableOnlyOfficeEditing:
type: boolean
enforceDarkLightMode:
description: '"dark" or "light"'
type: string
expires:
type: string
favicon:
@@ -751,6 +778,13 @@ definitions:
type: integer
downloadsLimit:
type: integer
enableOnlyOffice:
type: boolean
enableOnlyOfficeEditing:
type: boolean
enforceDarkLightMode:
description: '"dark" or "light"'
type: string
expire:
type: integer
favicon:
@@ -839,6 +873,9 @@ definitions:
autoplayMedia:
description: autoplay media files in preview
type: boolean
defaultMediaPlayer:
description: disable html5 media player and use the default media player
type: boolean
disableHideSidebar:
description: disable the hide sidebar preview for previews and editors
type: boolean
@@ -891,6 +928,9 @@ definitions:
description: when false, the date is relative, when true, the date is an exact
timestamp
type: boolean
debugOffice:
description: debug onlyoffice editor
type: boolean
deleteWithoutConfirming:
description: delete files without confirmation
type: boolean
@@ -901,7 +941,7 @@ definitions:
description: deprecated
type: string
disablePreviewExt:
description: comma separated list of file extensions to disable preview for
description: space separated list of file extensions to disable preview for
type: string
disableQuickToggles:
description: disable the quick toggles in the sidebar
@@ -915,7 +955,7 @@ definitions:
description: disable update notifications
type: boolean
disableViewingExt:
description: comma separated list of file extensions to disable viewing for
description: space separated list of file extensions to disable viewing for
type: string
editorQuickSave:
description: show quick save button in editor
@@ -1554,6 +1594,61 @@ paths:
summary: User login
tags:
- Auth
/api/media/subtitles:
get:
consumes:
- application/json
description: Extracts embedded subtitle content from video files by stream index
and returns raw WebVTT content
parameters:
- description: Index path to the video file
in: query
name: path
required: true
type: string
- description: Source name for the desired source
in: query
name: source
required: true
type: string
- description: Stream index for embedded subtitle extraction, defaults to 0
in: query
name: index
type: integer
produces:
- text/vtt
responses:
"200":
description: Raw WebVTT subtitle content
schema:
type: string
"400":
description: Bad request
schema:
additionalProperties:
type: string
type: object
"403":
description: Forbidden
schema:
additionalProperties:
type: string
type: object
"404":
description: Resource not found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Extract embedded subtitles
tags:
- Subtitles
/api/preview:
get:
consumes:
@@ -2077,6 +2172,31 @@ paths:
summary: Get system settings
tags:
- Settings
/api/settings/config:
get:
consumes:
- application/json
description: Returns the current running configuration settings as YAML format
with optional comments and filtering.
parameters:
- description: Show all values (true) or only non-default values (false, default)
in: query
name: full
type: boolean
- description: Include comments in YAML (true) or plain YAML (false, default)
in: query
name: comments
type: boolean
produces:
- text/plain
responses:
"200":
description: System settings in YAML format
schema:
type: string
summary: Get system settings as YAML
tags:
- Settings
/api/share:
get:
consumes:
-740
View File
@@ -1,740 +0,0 @@
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92478a24..5085e058 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,17 +11,16 @@ All notable changes to this project will be documented in this file. For commit
**New Features**:
- New access control system that works like shares. You can add new allow / deny rules for users/groups for specific paths on specific sources.
- #- The concept of 'groups', either automatically from OIDC groups claim or defined https://github.com/gtsteffaniak/filebrowser/issues/545
+ - The concept of 'groups', either automatically from OIDC groups claim or defined https://github.com/gtsteffaniak/filebrowser/issues/545
- share view changes -- now aligns with the standard listing view. This means files can be viewed and edited (if permission allows) just like a normal listing.
- many share links customization enhancements
- #- only share to certain authenticated users https://github.com/gtsteffaniak/filebrowser/issues/656 https://github.com/gtsteffaniak/filebrowser/issues/985
- #- one-time download links
- #- customize share theme, banner image, and icons https://github.com/gtsteffaniak/filebrowser/issues/827 https://github.com/gtsteffaniak/filebrowser/issues/1029
- #- share link info customization https://github.com/gtsteffaniak/filebrowser/issues/792 https://github.com/gtsteffaniak/filebrowser/issues/841 https://github.com/gtsteffaniak/filebrowser/issues/520
- #- "shared with me" and "share history" https://github.com/gtsteffaniak/filebrowser/issues/943
+ - only share to certain authenticated users https://github.com/gtsteffaniak/filebrowser/issues/656 https://github.com/gtsteffaniak/filebrowser/issues/985
+ - one-time download links
+ - customize share theme, banner image, and icons https://github.com/gtsteffaniak/filebrowser/issues/827 https://github.com/gtsteffaniak/filebrowser/issues/1029
+ - share link info customization https://github.com/gtsteffaniak/filebrowser/issues/792 https://github.com/gtsteffaniak/filebrowser/issues/841 https://github.com/gtsteffaniak/filebrowser/issues/520
+ - "shared with me" and "share history" https://github.com/gtsteffaniak/filebrowser/issues/943
- upload to share links https://github.com/gtsteffaniak/filebrowser/issues/661
- share link public changes https://github.com/gtsteffaniak/filebrowser/issues/473
- -
- public/private folder logic https://github.com/gtsteffaniak/filebrowser/issues/505
**BugFixes**:
diff --git a/backend/http/middleware.go b/backend/http/middleware.go
index 5ba397fe..595f00e1 100644
--- a/backend/http/middleware.go
+++ b/backend/http/middleware.go
@@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"net/http"
- "net/url"
"runtime"
"strings"
"time"
@@ -44,14 +43,8 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, data *requestContex
// Middleware to handle file requests by hash and pass it to the handler
func withHashFileHelper(fn handleFunc) handleFunc {
return withOrWithoutUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
+ path := r.URL.Query().Get("path")
hash := r.URL.Query().Get("hash")
- encodedPath := r.URL.Query().Get("path")
- // Decode the URL-encoded path
- path, err := url.QueryUnescape(encodedPath)
- if err != nil {
- return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
- }
- data.path = path
// Get the file link by hash
link, err := store.Share.GetByHash(hash)
if err != nil {
@@ -71,6 +64,11 @@ func withHashFileHelper(fn handleFunc) handleFunc {
return status, fmt.Errorf("could not authenticate share request")
}
}
+ data.path = strings.TrimSuffix(link.Path, "/") + "/" + strings.TrimPrefix(path, "/")
+ if path == "" || path == "/" {
+ data.path = link.Path
+ }
+
source, ok := config.Server.SourceMap[link.Source]
if !ok {
return http.StatusNotFound, fmt.Errorf("source not found")
diff --git a/backend/http/middleware_test.go b/backend/http/middleware_test.go
index ff7c173e..eb257951 100644
--- a/backend/http/middleware_test.go
+++ b/backend/http/middleware_test.go
@@ -155,20 +155,6 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
},
expectedStatusCode: 0, // zero means 200 on helpers
},
- {
- name: "Private share, valid password when token exists",
- share: &share.Link{
- Hash: "pw_and_token_hash",
- UserID: 1,
- PasswordHash: passwordBcrypt,
- Token: "some_random_token",
- Source: "/srv",
- },
- extraHeaders: map[string]string{
- "X-SHARE-PASSWORD": "password",
- },
- expectedStatusCode: 0, // zero means 200 on helpers
- },
{
name: "Private share, no auth provided",
share: &share.Link{
diff --git a/backend/http/public.go b/backend/http/public.go
index ef65dfa6..0e641fbd 100644
--- a/backend/http/public.go
+++ b/backend/http/public.go
@@ -7,6 +7,9 @@ import (
"net/url"
"strings"
+ "github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
+ "github.com/gtsteffaniak/filebrowser/backend/common/utils"
+ "github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
"github.com/gtsteffaniak/go-logger/logger"
_ "github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
@@ -49,7 +52,7 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
}
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
- d.fileInfo.Path = "/" + strings.TrimPrefix(d.fileInfo.Path, d.share.Path)
+ d.fileInfo.Path = strings.TrimPrefix(d.fileInfo.Path, d.share.Path)
return renderJSON(w, r, d.fileInfo)
}
@@ -91,5 +94,24 @@ func publicPreviewHandler(w http.ResponseWriter, r *http.Request, d *requestCont
if config.Server.DisablePreviews {
return http.StatusNotImplemented, fmt.Errorf("preview is disabled")
}
+ path := r.URL.Query().Get("path")
+ var err error
+ if path == "" {
+ return http.StatusBadRequest, fmt.Errorf("invalid request path")
+ }
+ fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{
+ Path: utils.JoinPathAsUnix(d.share.Path, path),
+ Modify: false, // TODO make this configurable
+ Source: d.fileInfo.Source,
+ Expand: true,
+ })
+ if err != nil {
+ logger.Debugf("public preview handler: error getting file info: %v", err)
+ return 400, fmt.Errorf("file not found")
+ }
+ if fileInfo.Type == "directory" {
+ return http.StatusBadRequest, fmt.Errorf("can't create preview for directory")
+ }
+ d.fileInfo = fileInfo
return previewHelperFunc(w, r, d)
}
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index 9399a292..2ef5f38e 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -7,3 +7,6 @@ import search from "./search";
// Note: shareApi has been consolidated into publicApi
export { filesApi, publicApi, usersApi, settingsApi, search, accessApi };
+
+// For backward compatibility, export publicApi as shareApi as well
+export { publicApi as shareApi };
diff --git a/frontend/src/api/public.js b/frontend/src/api/public.js
index 1dcb4125..d94713c5 100644
--- a/frontend/src/api/public.js
+++ b/frontend/src/api/public.js
@@ -2,7 +2,6 @@ import { fetchURL, fetchJSON, adjustedData } from "./utils";
import { notify } from "@/notify";
import { getApiPath, removePrefix } from "@/utils/url.js";
import { externalUrl, baseURL } from "@/utils/constants";
-import { state } from "@/store";
// ============================================================================
// PUBLIC API ENDPOINTS (hash-based authentication)
@@ -13,8 +12,7 @@ export async function fetchPub(path, hash, password = "", content = false) {
const params = {
path,
hash,
- ...(content && { content: 'true' }),
- ...(state.share.token && { token: state.share.token })
+ ...(content && { content: 'true' })
}
const apiPath = getApiPath("public/api/shared", params);
const response = await fetch(apiPath, {
@@ -24,17 +22,7 @@ export async function fetchPub(path, hash, password = "", content = false) {
});
if (!response.ok) {
- const error = new Error(response.statusText);
- // attempt to marshal json response
- let data = null;
- try {
- data = await response.json()
- } catch (e) {
- // ignore
- }
- if (data) {
- error.message = data.message;
- }
+ const error = new Error("Failed to connect to the server.");
error.status = response.status;
throw error;
}
@@ -57,14 +45,13 @@ export function getDownloadURL(share, files) {
}
// Generate a preview URL for public shares
-export function getPreviewURL(path) {
+export function getPreviewURL(hash, path) {
try {
const params = {
path: encodeURIComponent(path),
size: 'small',
- hash: state.share.hash,
- inline: 'true',
- ...(state.share.token && { token: state.share.token })
+ hash: hash,
+ inline: 'true'
}
const apiPath = getApiPath('public/api/preview', params)
return window.origin + apiPath
diff --git a/frontend/src/api/share.js b/frontend/src/api/share.js
new file mode 100644
index 00000000..729b7599
--- /dev/null
+++ b/frontend/src/api/share.js
@@ -0,0 +1,66 @@
+import { fetchURL, fetchJSON, adjustedData } from "./utils";
+import { notify } from "@/notify";
+import { getApiPath } from "@/utils/url.js";
+import { externalUrl } from "@/utils/constants";
+
+export async function list() {
+ const apiPath = getApiPath("api/shares");
+ return fetchJSON(apiPath);
+}
+
+export async function get(path, source) {
+ try {
+ const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
+ const apiPath = getApiPath("api/share",params);
+ let data = fetchJSON(apiPath);
+ return adjustedData(data, path);
+ } catch (err) {
+ notify.showError(err.message || "Error fetching data");
+ throw err;
+ }
+}
+
+export async function remove(hash) {
+ const params = { hash };
+ const apiPath = getApiPath("api/share",params);
+ await fetchURL(apiPath, {
+ method: "DELETE",
+ });
+}
+
+export async function create(path, source, password = "", expires = "", unit = "hours") {
+ const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
+ const apiPath = getApiPath("api/share",params);
+ let body = "{}";
+ if (password != "" || expires !== "" || unit !== "hours") {
+ body = JSON.stringify({ password: password, expires: expires, unit: unit });
+ }
+ return fetchJSON(apiPath, {
+ method: "POST",
+ body: body,
+ });
+}
+
+export function getShareURL(share) {
+ if (externalUrl) {
+ const apiPath = getApiPath(`share/${share.hash}`)
+ return externalUrl + apiPath
+ }
+ return window.origin+getApiPath(`share/${share.hash}`);
+}
+
+export function getPreviewURL(hash, path) {
+ try {
+ const params = {
+ path: encodeURIComponent(path),
+ size: 'small',
+ hash: hash,
+ inline: 'true'
+ }
+ const apiPath = getApiPath('api/public/preview', params)
+ return window.origin + apiPath
+ } catch (err) {
+ notify.showError(err.message || 'Error getting preview URL')
+ throw err
+ }
+}
diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue
index 29c2749f..93848c1c 100644
--- a/frontend/src/components/files/ListingItem.vue
+++ b/frontend/src/components/files/ListingItem.vue
@@ -67,7 +67,7 @@ import { enableThumbs } from "@/utils/constants";
import downloadFiles from "@/utils/download";
import { getHumanReadableFilesize } from "@/utils/filesizes";
-import { filesApi,publicApi } from "@/api";
+import { filesApi, shareApi } from "@/api";
import * as upload from "@/utils/upload";
import { state, getters, mutations } from "@/store"; // Import your custom store
import { url } from "@/utils";
@@ -162,8 +162,8 @@ export default {
// @ts-ignore
let path = url.removeTrailingSlash(state.req.path) + "/" + this.name;
if (getters.isShare()) {
- let urlPath = getters.getSharePath(this.name) ;
- return publicApi.getPreviewURL(urlPath);
+ let urlPath = getters.getSharePath() + "/" + this.name;
+ return shareApi.getPreviewURL(state.share.hash, urlPath, state.req.modified);
}
// @ts-ignore
return filesApi.getPreviewURL(state.req.source, path, this.modified);
@@ -226,7 +226,7 @@ export default {
},
getUrl() {
if (this.hash) {
- return baseURL + "public/share/" + this.hash + "/" + url.encodedPath(this.path);
+ return baseURL + "share/" + this.hash + url.encodedPath(this.path);
}
if (serverHasMultipleSources) {
return baseURL + "files/" + encodeURIComponent(this.source) + url.encodedPath(this.path);
diff --git a/frontend/src/components/prompts/ActionApi.vue b/frontend/src/components/prompts/ActionApi.vue
index f2ebaa3f..1a381565 100644
--- a/frontend/src/components/prompts/ActionApi.vue
+++ b/frontend/src/components/prompts/ActionApi.vue
@@ -90,3 +90,20 @@ export default {
},
};
</script>
+
+<style>
+/* Basic styling for the prompt */
+.card.floating {
+ padding: 20px;
+}
+.card-content {
+ margin-bottom: 16px;
+}
+.card-action {
+ display: flex;
+ justify-content: flex-end;
+}
+.card-action .button {
+ margin-left: 8px;
+}
+</style>
diff --git a/frontend/src/components/prompts/Share.vue b/frontend/src/components/prompts/Share.vue
index be1ad5ad..8689100d 100644
--- a/frontend/src/components/prompts/Share.vue
+++ b/frontend/src/components/prompts/Share.vue
@@ -2,11 +2,10 @@
<div class="card-title">
<h2>{{ $t("buttons.share") }}</h2>
</div>
- <div class="card-content">
- <div aria-label="share-path" class="searchContext"> {{ $t('search.path') }} {{ subpath }}</div>
- <p> {{ $t('share.notice') }} </p>
-
- <div v-if="listing">
+ <div aria-label="share-path" class="searchContext"> {{ $t('search.path') }} {{ subpath }}</div>
+ <p> {{ $t('share.notice') }} </p>
+ <template v-if="listing">
+ <div class="card-content">
<table>
<tbody>
<tr>
@@ -46,7 +45,20 @@
</table>
</div>
- <div v-else>
+ <div class="card-action">
+ <button class="button button--flat button--grey" @click="closeHovers" :aria-label="$t('buttons.close')"
+ :title="$t('buttons.close')">
+ {{ $t("buttons.close") }}
+ </button>
+ <button class="button button--flat button--blue" @click="() => switchListing()" :aria-label="$t('buttons.new')"
+ :title="$t('buttons.new')">
+ {{ $t("buttons.new") }}
+ </button>
+ </div>
+ </template>
+
+ <template v-else>
+ <div class="card-content">
<p>{{ $t("settings.shareDuration") }}</p>
<div class="form-flex-group">
<input class="form-grow input flat-right" v-focus type="number" max="2147483647" min="1" @keyup.enter="submit" v-model.trim="time" />
@@ -58,7 +70,7 @@
</select>
</div>
<p>{{ $t("prompts.optionalPassword") }}</p>
- <input class="input" type="password" autocomplete="off" v-model.trim="password" />
+ <input class="input" type="password" v-model.trim="password" />
<div class="settings-items">
<ToggleSwitch class="item" v-model="readOnly" :name="'Read-only access'" />
@@ -80,32 +92,23 @@
</select>
</div>
</div>
- </div>
- <div class="card-action">
- <button v-if="listing" class="button button--flat button--grey" @click="closeHovers" :aria-label="$t('buttons.close')"
- :title="$t('buttons.close')">
- {{ $t("buttons.close") }}
- </button>
- <button v-if="listing" class="button button--flat button--blue" @click="() => switchListing()" :aria-label="$t('buttons.new')"
- :title="$t('buttons.new')">
- {{ $t("buttons.new") }}
- </button>
-
- <button v-if="!listing" class="button button--flat button--grey" @click="() => switchListing()" :aria-label="$t('buttons.cancel')"
- :title="$t('buttons.cancel')">
- {{ $t("buttons.cancel") }}
- </button>
- <button v-if="!listing" class="button button--flat button--blue" @click="submit" aria-label="Share-Confirm"
- :title="$t('buttons.share')">
- {{ $t("buttons.share") }}
- </button>
- </div>
+ <div class="card-action">
+ <button class="button button--flat button--grey" @click="() => switchListing()" :aria-label="$t('buttons.cancel')"
+ :title="$t('buttons.cancel')">
+ {{ $t("buttons.cancel") }}
+ </button>
+ <button class="button button--flat button--blue" @click="submit" aria-label="Share-Confirm"
+ :title="$t('buttons.share')">
+ {{ $t("buttons.share") }}
+ </button>
+ </div>
+ </template>
</template>
<script>
import { notify } from "@/notify";
import { state, getters, mutations } from "@/store";
-import { publicApi } from "@/api";
+import { shareApi, publicApi } from "@/api";
import Clipboard from "clipboard";
import { fromNow } from "@/utils/moment";
import { buildItemUrl } from "@/utils/url";
@@ -188,7 +191,7 @@ export default {
try {
// get last element of the path
- const links = await publicApi.get(this.subpath, this.source);
+ const links = await shareApi.get(this.subpath, this.source);
this.links = links;
} catch (err) {
notify.showError(err);
@@ -215,9 +218,9 @@ export default {
let isPermanent = !this.time || this.time == 0;
let res = null;
if (isPermanent) {
- res = await publicApi.create(this.subpath, this.source, this.password);
+ res = await shareApi.create(this.subpath, this.source, this.password);
} else {
- res = await publicApi.create(
+ res = await shareApi.create(
this.subpath,
this.source,
this.password,
@@ -241,7 +244,7 @@ export default {
async deleteLink(event, link) {
event.preventDefault();
try {
- await publicApi.remove(link.hash);
+ await shareApi.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length === 0) {
this.listing = false;
@@ -254,7 +257,7 @@ export default {
return fromNow(time, state.user.locale)
},
buildLink(share) {
- return publicApi.getShareURL(share);
+ return shareApi.getShareURL(share);
},
hasDownloadLink() {
if (state.isSearchActive) {
diff --git a/frontend/src/components/sidebar/General.vue b/frontend/src/components/sidebar/General.vue
index 58f02c10..0e030aeb 100644
--- a/frontend/src/components/sidebar/General.vue
+++ b/frontend/src/components/sidebar/General.vue
@@ -159,7 +159,7 @@ export default {
isStickySidebar: () => getters.isStickySidebar(),
isMobile: () => getters.isMobile(),
isListingView: () => getters.currentView() == "listingView",
- user: () => (state.user),
+ user: () => (console.log(state.user), state.user),
isDarkMode: () => getters.isDarkMode(),
showSources: () => !getters.isShare(),
currentPrompt: () => getters.currentPrompt(),
diff --git a/frontend/src/css/dashboard.css b/frontend/src/css/dashboard.css
index 2e1fbc8f..77da5938 100644
--- a/frontend/src/css/dashboard.css
+++ b/frontend/src/css/dashboard.css
@@ -226,10 +226,6 @@ body.rtl .card-title>*:first-child {
overflow: auto;
}
-.card-content h3 {
- text-align: center;
-}
-
.card h2 {
font-weight: 500;
}
diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.js
index cb062702..d97e7eed 100644
--- a/frontend/src/store/getters.js
+++ b/frontend/src/store/getters.js
@@ -201,9 +201,10 @@ export const getters = {
sharePathBase: () => {
return '/public/share/' + getters.shareHash() + '/'
},
- getSharePath: (subPath = "") => {
+ getSharePath: () => {
let urlPath = getters.routePath('public/share')
- return "/" + removeLeadingSlash(urlPath.split(state.share.hash)[1]) + "/" + subPath
+ let parts = urlPath.split('/')
+ return "/" + removeLeadingSlash(parts.slice(2).join('/'))
},
currentView: () => {
let listingView = ''
@@ -288,6 +289,7 @@ export const getters = {
},
currentPromptName: () => {
+ console.log("currentPromptName", state.prompts);
// Ensure state.prompts is an array
if (!Array.isArray(state.prompts) || state.prompts.length === 0) {
return ""
diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.js
index 8dc0b19f..b2b13461 100644
--- a/frontend/src/store/mutations.js
+++ b/frontend/src/store/mutations.js
@@ -191,6 +191,7 @@ export const mutations = {
emitStateChanged();
},
showHover: (value) => {
+ console.log("showHover", value);
if (typeof value === "object") {
state.prompts.push({
name: value?.name,
@@ -206,6 +207,7 @@ export const mutations = {
props: value?.props,
});
}
+ console.log("showHover", state.prompts);
emitStateChanged();
},
setLoading: (loadType, status) => {
@@ -221,6 +223,7 @@ export const mutations = {
emitStateChanged();
},
setCurrentUser: (value) => {
+ console.log('setCurrentUser', value);
try {
// If value is null or undefined, emit state change and exit early
if (!value) {
@@ -243,6 +246,7 @@ export const mutations = {
} catch (error) {
console.log(error);
}
+ console.log('setCurrentUser', state.user);
emitStateChanged();
},
setJWT: (value) => {
@@ -303,6 +307,7 @@ export const mutations = {
if (!state.user) {
state.user = {};
}
+ console.log('updateCurrentUser', value);
// Store previous state for comparison
const previousUser = { ...state.user };
@@ -315,12 +320,14 @@ export const mutations = {
i18n.default.locale = state.user.locale;
localStorage.setItem("userLocale", state.user.locale);
}
+ console.log('updateCurrentUser2', state.user);
// Update localStorage if stickySidebar exists
if (state.user.stickySidebar && getters.currentView() == "listingView") {
state.multiButtonState = "menu";
} else if (state.showSidebar) {
state.multiButtonState = "back";
}
+ console.log('updateCurrentUser3', state.user);
// Update users if there's any change in state.user
if (JSON.stringify(state.user) !== JSON.stringify(previousUser)) {
// Only update the properties that were actually provided in the input
diff --git a/frontend/src/views/Files.vue b/frontend/src/views/Files.vue
index 4c69ecd7..6f8b892b 100644
--- a/frontend/src/views/Files.vue
+++ b/frontend/src/views/Files.vue
@@ -9,9 +9,8 @@
<div class="card-title">
<h2>{{ $t("general.password") }}</h2>
</div>
- <div class="card-content form-flex-group">
+ <div class="card-content">
<input
- class="input share-password"
v-focus
type="password"
:placeholder="$t('general.password')"
@@ -149,10 +148,8 @@ export default {
});
// Add glow effect
element.classList.add('scroll-glow');
- console.log("scrollToHash", element)
// Remove glow effect after animation completes
setTimeout(() => {
- console.log("scrollToHash remove glow", element)
element.classList.remove('scroll-glow');
}, 1000);
}
@@ -211,15 +208,19 @@ export default {
this.shareHash = parts[1]
this.shareSubPath = "/" + parts.slice(2).join("/");
+ // Store share data in state for use by components
+ mutations.setShareData({
+ hash: this.shareHash,
+ token: this.shareToken,
+ subPath: this.shareSubPath,
+ });
+
// Handle password
- if (this.sharePassword === "") {
+ if (this.sharePassword === "" || this.sharePassword === null) {
this.sharePassword = localStorage.getItem("sharepass:" + this.shareHash);
} else {
localStorage.setItem("sharepass:" + this.shareHash, this.sharePassword);
}
- if (this.sharePassword === null) {
- this.sharePassword = "";
- }
mutations.resetSelected();
mutations.setMultiple(false);
@@ -229,15 +230,11 @@ export default {
let file = await publicApi.fetchPub(this.shareSubPath, this.shareHash, this.sharePassword);
file.hash = this.shareHash;
this.shareToken = file.token;
- // Store share data in state for use by components
- mutations.setShareData({
- hash: this.shareHash,
- token: this.shareToken,
- subPath: this.shareSubPath,
- });
+
// If not a directory, fetch content for preview components
if (file.type != "directory") {
const content = !getters.fileViewingDisabled(file.name);
+
file = await publicApi.fetchPub(this.shareSubPath, this.shareHash, this.sharePassword, content);
file.hash = this.shareHash;
this.shareToken = file.token;
@@ -324,7 +321,7 @@ export default {
};
</script>
-<style>
+<style scoped>
.share__wrong__password {
color: #ff4757;
text-align: center;
@@ -351,8 +348,4 @@ export default {
color: inherit;
}
}
-
-.share-password {
- width: 100%;
-}
</style>
diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue
index 4547e3d2..38d75dc1 100644
--- a/frontend/src/views/Settings.vue
+++ b/frontend/src/views/Settings.vue
@@ -122,4 +122,7 @@ export default {
background-color: var(--surfaceSecondary);
}
+.card-content {
+ padding: 1em;
+}
</style>
diff --git a/frontend/src/views/settings/Profile.vue b/frontend/src/views/settings/Profile.vue
index f65fe30f..e34e405a 100644
--- a/frontend/src/views/settings/Profile.vue
+++ b/frontend/src/views/settings/Profile.vue
@@ -428,6 +428,9 @@ export default {
</script>
<style scoped>
+.card-content h3 {
+ text-align: center;
+}
#disablePreviews,
#disableViewing,
diff --git a/frontend/src/views/settings/Shares.vue b/frontend/src/views/settings/Shares.vue
index cd28af70..6d39166e 100644
--- a/frontend/src/views/settings/Shares.vue
+++ b/frontend/src/views/settings/Shares.vue
@@ -49,7 +49,7 @@
<script>
import { notify } from "@/notify";
-import { publicApi } from "@/api";
+import { shareApi } from "@/api";
import { state, mutations, getters } from "@/store";
import Clipboard from "clipboard";
import Errors from "@/views/Errors.vue";
@@ -70,7 +70,7 @@ export default {
async created() {
mutations.setLoading("shares", true);
try {
- let links = await publicApi.list();
+ let links = await shareApi.list();
if (links.length === 0) {
this.links = [];
return;
@@ -115,7 +115,7 @@ export default {
mutations.closeHovers();
try {
- publicApi.remove(link.hash);
+ shareApi.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
notify.showSuccess(this.$t("settings.shareDeleted"));
} catch (e) {
@@ -128,7 +128,7 @@ export default {
return fromNow(time);
},
buildLink(share) {
- return publicApi.getShareURL(share);
+ return shareApi.getShareURL(share);
},
},
};
-1
View File
@@ -1 +0,0 @@
frontend
+5 -4
View File
@@ -8,11 +8,10 @@
"node": ">=18.0.0"
},
"scripts": {
"dev": "vite dev",
"build": "vite build && cp -r dist/* ../backend/http/embed",
"build-windows": "vite build && robocopy dist ../backend/http/embed /e",
"build": "vite build --outDir ../backend/http/dist && cp -r ../backend/http/dist/* ../backend/http/embed",
"build-windows": "vite build --outDir ../backend/http/dist && robocopy ../backend/http/dist ../backend/http/embed /e",
"build-docker": "vite build",
"watch": "vite build --watch",
"watch": "vite build --watch --outDir ../backend/http/dist",
"typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
"lint": "eslint --ext .js,.vue,ts src",
"lint:fix": "eslint --fix src/",
@@ -22,6 +21,7 @@
},
"dependencies": {
"@onlyoffice/document-editor-vue": "^1.4.0",
"@skjnldsv/vue-plyr": "^7.5.0",
"ace-builds": "^1.24.2",
"axios": "^1.7.9",
"clipboard": "^2.0.4",
@@ -31,6 +31,7 @@
"file-loader": "^6.2.0",
"glob": "^9.3.5",
"highlight.js": "^11.11.1",
"jsmediatags": "github:Kurami32/jsmediatags",
"mammoth": "^1.9.1",
"marked": "^15.0.6",
"normalize.css": "^8.0.1",
+22 -17
View File
@@ -1,4 +1,6 @@
server:
minSearchLength: 3 # minimum length of search query to begin searching (default: 3)
disableUpdateCheck: false # disables backend update check service
numImageProcessors: 4 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
socket: "" # socket to listen on
tlsKey: "" # path to TLS key
@@ -48,7 +50,7 @@ server:
cacheDir: "tmp" # path to the cache directory, used for thumbnails and other cached files
maxArchiveSize: 50 # max pre-archive combined size of files/folder that are allowed to be archived (in GB)
auth:
tokenExpirationHours: 2 # the number of hours until the token expires. Default is 2 hours.
tokenExpirationHours: 2 # time in hours each web UI session token is valid for. Default is 2 hours.
methods:
proxy: # validate:omitempty
enabled: false
@@ -67,8 +69,8 @@ auth:
enforcedOtp: false # if set to true, TOTP is enforced for all password users users. Otherwise, users can choose to enable TOTP.
oidc: # validate:omitempty
enabled: false # whether to enable OIDC authentication
clientId: "" # client id of the OIDC application
clientSecret: "" # client secret of the OIDC application
clientId: "**hidden**" # secret: client id of the OIDC application
clientSecret: "**hidden**" # secret: client secret of the OIDC application
issuerUrl: "" # authorization URL of the OIDC provider
scopes: "openid email profile" # scopes to request from the OIDC provider
userIdentifier: "preferred_username" # the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone"
@@ -77,10 +79,10 @@ auth:
createUser: false # create user if not exists
adminGroup: "" # if set, users in this group will be granted admin privileges.
groupsClaim: "groups" # the JSON field name to read groups from. Default is "groups"
key: "" # the key used to sign the JWT tokens. If not set, a random key will be generated.
adminUsername: "admin" # the username of the admin user. If not set, the default is "admin".
adminPassword: "admin" # the password of the admin user. If not set, the default is "admin".
totpSecret: "" # secret used to encrypt TOTP secrets
key: "**hidden**" # secret: the key used to sign the JWT tokens. If not set, a random key will be generated.
adminUsername: "**hidden**" # secret: the username of the admin user. If not set, the default is "admin".
adminPassword: "**hidden**" # secret: the password of the admin user. If not set, the default is "admin".
totpSecret: "**hidden**" # secret: secret used to encrypt TOTP secrets
frontend:
name: "FileBrowser Quantum" # display name
disableDefaultLinks: false # disable default links in the sidebar
@@ -91,19 +93,21 @@ frontend:
url: "https://github.com/gtsteffaniak/filebrowser/releases/" # the url to link to validate:required
- text: "Help" # the text to display on the link validate:required
title: "" # the title to display on hover
url: "https://github.com/gtsteffaniak/filebrowser/wiki" # the url to link to validate:required
url: "help prompt" # the url to link to validate:required
disableNavButtons: false # disable the nav buttons in the sidebar
styling:
customCSS: "" # if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css")
lightBackground: "#f5f5f5" # specify a valid CSS color property value to use as the background color in light mode
darkBackground: "#141D24" # Specify a valid CSS color property value to use as the background color in dark mode
customThemes: # A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option.
alternative:
description: Reduce rounded corners
css: reduce-rounded-corners.css
default:
description: The default theme
css: ""
description: "The default theme" # The description of the theme to display in the UI.
css: "" # The css file path and filename to use for the theme.
alternative:
description: "Reduce rounded corners" # The description of the theme to display in the UI.
css: "reduce-rounded-corners.css" # The css file path and filename to use for the theme.
favicon: "" # path to a favicon to use for the frontend
description: "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server" # description that shows up in html head meta description
userDefaults:
editorQuickSave: false # show quick save button in editor
hideSidebarFileActions: false # hide the file actions in the sidebar
@@ -119,8 +123,8 @@ userDefaults:
gallerySize: 3 # 0-9 - the size of the gallery thumbnails
themeColor: "var(--blue)" # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
quickDownload: false # show icon to download in one click
disablePreviewExt: "" # comma separated list of file extensions to disable preview for
disableViewingExt: "" # comma separated list of file extensions to disable viewing for
disablePreviewExt: "" # space separated list of file extensions to disable preview for
disableViewingExt: "" # space separated list of file extensions to disable viewing for
lockPassword: false # disable the user from changing their password
disableSettings: false # disable the user from viewing the settings page
preview:
@@ -132,6 +136,7 @@ userDefaults:
office: false
popup: false
autoplayMedia: false
defaultMediaPlayer: false
permissions:
api: false
admin: false
@@ -144,14 +149,14 @@ userDefaults:
fileLoading: # upload and download settings
maxConcurrentUpload: 10
uploadChunkSizeMb: 10
disableOfficePreviewExt: "" # deprecated: use disablePreviewExt instead
disableOnlyOfficeExt: "" # 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
integrations:
office: # validate:omitempty
url: "" # The URL to the OnlyOffice Document Server, needs to be accessible to the user. validate:required
internalUrl: "" # An optional internal address that the filebrowser server can use to communicate with the OnlyOffice Document Server, could be useful to bypass proxy.
secret: "" # validate:required
secret: "**hidden**" # secret: authentication key for OnlyOffice integration validate:required
media: # validate:omitempty
ffmpegPath: "" # path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)
+18 -5
View File
@@ -50,9 +50,15 @@ async function translateText(text, targetLanguage, keyPath = '') {
return text;
}
if (/\{[^}]+\}/.test(text)) {
console.log(`Skipping translation for placeholder string: "${text}"`);
return text;
const hasPlaceholders = /\{[^}]+\}/.test(text);
let textToTranslate = text;
const options = {};
if (hasPlaceholders) {
console.log(`Found placeholder in: "${text}". Wrapping for translation.`);
textToTranslate = text.replace(/(\{[^}]+\})/g, '<ph>$1</ph>');
options.tagHandling = 'xml';
options.ignoreTags = ['ph'];
}
try {
@@ -60,12 +66,19 @@ async function translateText(text, targetLanguage, keyPath = '') {
let deeplTargetLang = deeplLangMap[targetLanguage.toLowerCase()] || targetLanguage.toUpperCase();
const result = await translator.translateText(text, masterLanguageCode, deeplTargetLang);
const result = await translator.translateText(textToTranslate, masterLanguageCode, deeplTargetLang, options);
// Delay to avoid rate-limiting
await new Promise(resolve => setTimeout(resolve, 100));
return result.text;
let translatedText = result.text;
if (hasPlaceholders) {
// Unwrap the placeholders. The translator might add spaces around tags.
translatedText = translatedText.replace(/<ph>\s*(\{[^}]+\})\s*<\/ph>/g, '$1');
}
return translatedText;
} catch (err) {
console.error(`⚠️ Translation failed for "${text}" (${keyPath}):`, err?.message || err);
+15 -1
View File
@@ -1,5 +1,5 @@
import { fetchURL, adjustedData } from './utils'
import { getApiPath, doubleEncode } from '@/utils/url.js'
import { getApiPath, doubleEncode, getPublicApiPath } from '@/utils/url.js'
import { mutations } from '@/store'
import { notify } from '@/notify'
import { externalUrl } from '@/utils/constants'
@@ -265,3 +265,17 @@ export async function sources() {
throw err
}
}
export async function GetOfficeConfig(req) {
const params = {
path: encodeURIComponent(req.path),
...(req.hash && { hash: encodeURIComponent(req.hash) }),
...(req.source && { source: encodeURIComponent(req.source) })
}
let apiPath = getApiPath('api/onlyoffice/config', params)
if (req.hash) {
apiPath = getPublicApiPath('onlyoffice/config', params)
}
const res = await fetchURL(apiPath)
return await res.json()
}
+2 -1
View File
@@ -3,7 +3,8 @@ import * as publicApi from "./public";
import * as usersApi from "./users";
import * as settingsApi from "./settings";
import * as accessApi from "./access";
import * as shareApi from "./share";
import search from "./search";
// Note: shareApi has been consolidated into publicApi
export { filesApi, publicApi, usersApi, settingsApi, search, accessApi };
export { filesApi, publicApi, usersApi, settingsApi, shareApi, search, accessApi };
+4 -74
View File
@@ -1,4 +1,4 @@
import { fetchURL, fetchJSON, adjustedData } from "./utils";
import { adjustedData } from "./utils";
import { notify } from "@/notify";
import { getApiPath, getPublicApiPath } from "@/utils/url.js";
import { externalUrl } from "@/utils/constants";
@@ -57,8 +57,10 @@ export async function fetchPub(path, hash, password = "", content = false) {
* @returns {string}
*/
export function getDownloadURL(share, files, inline=false) {
// Join files array with || delimiter and then URL encode
const filesParam = Array.isArray(files) ? files.join('||') : files;
const params = {
files: files,
files: encodeURIComponent(filesParam),
hash: share.hash,
token: share.token,
...(inline && { inline: 'true' })
@@ -89,60 +91,6 @@ export function getPreviewURL(path,size="small") {
}
}
// ============================================================================
// SHARE MANAGEMENT API (permission-based authentication)
// ============================================================================
// List all shares
export async function list() {
const apiPath = getApiPath("public/shares");
return fetchJSON(apiPath);
}
// Get share information
/**
* @param {string} path
* @param {string} source
* @returns {Promise<any>}
*/
export async function get(path, source) {
try {
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
const apiPath = getApiPath("public/share", params);
let data = await fetchJSON(apiPath);
return adjustedData(data);
} catch (/** @type {any} */ err) {
notify.showError(err.message || "Error fetching data");
throw err;
}
}
// Remove/delete a share
/**
* @param {string} hash
* @returns {Promise<void>}
*/
export async function remove(hash) {
const params = { hash };
const apiPath = getApiPath("public/share", params);
await fetchURL(apiPath, {
method: "DELETE",
});
}
// Create a new share
/**
* @param {Record<string, any>} bodyObj
* @returns {Promise<Share>}
*/
export async function create(bodyObj = {}) {
const apiPath = getApiPath("public/share");
return fetchJSON(apiPath, {
method: "POST",
body: JSON.stringify(bodyObj || {}),
});
}
// Get the shareable URL for a share
/**
* @param {{ hash: string }} share
@@ -155,21 +103,3 @@ export function getShareURL(share) {
}
return window.origin + getApiPath(`public/share/${share.hash}`);
}
/**
* @typedef {object} Share
* @property {string} hash
* @property {string} path
* @property {string} source
* @property {number} expire
* @property {number} downloadsLimit
* @property {number} maxBandwidth
* @property {string} shareTheme
* @property {boolean} disableAnonymous
* @property {boolean} disableThumbnails
* @property {boolean} keepAfterExpiration
* @property {string[]} allowedUsernames
* @property {string} viewMode
* @property {string} token
* @property {boolean} inline
*/
+8
View File
@@ -13,3 +13,11 @@ export async function update(settings) {
body: JSON.stringify(settings),
});
}
export function getConfig(showFull = false, showComments = false) {
const params = {};
if (showFull) params.full = "true";
if (showComments) params.comments = "true";
const path = getApiPath("api/settings/config", params);
return fetchURL(path);
}
+76
View File
@@ -0,0 +1,76 @@
import { fetchURL, fetchJSON, adjustedData } from "./utils";
import { notify } from "@/notify";
import { getApiPath } from "@/utils/url.js";
// ============================================================================
// SHARE MANAGEMENT API (permission-based authentication)
// ============================================================================
// List all shares
export async function list() {
const apiPath = getApiPath("api/shares");
return fetchJSON(apiPath);
}
// Get share information
/**
* @param {string} path
* @param {string} source
* @returns {Promise<any>}
*/
export async function get(path, source) {
try {
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
const apiPath = getApiPath("api/share", params);
let data = await fetchJSON(apiPath);
return adjustedData(data);
} catch (/** @type {any} */ err) {
notify.showError(err.message || "Error fetching data");
throw err;
}
}
// Remove/delete a share
/**
* @param {string} hash
* @returns {Promise<void>}
*/
export async function remove(hash) {
const params = { hash };
const apiPath = getApiPath("api/share", params);
await fetchURL(apiPath, {
method: "DELETE",
});
}
// Create a new share
/**
* @param {Record<string, any>} bodyObj
* @returns {Promise<Share>}
*/
export async function create(bodyObj = {}) {
const apiPath = getApiPath("api/share");
return fetchJSON(apiPath, {
method: "POST",
body: JSON.stringify(bodyObj || {}),
});
}
/**
* @typedef {object} Share
* @property {string} hash
* @property {string} path
* @property {string} source
* @property {number} expire
* @property {number} downloadsLimit
* @property {number} maxBandwidth
* @property {string} shareTheme
* @property {boolean} disableAnonymous
* @property {boolean} disableThumbnails
* @property {boolean} keepAfterExpiration
* @property {string[]} allowedUsernames
* @property {string} viewMode
* @property {string} token
* @property {boolean} inline
*/
+37 -16
View File
@@ -14,15 +14,25 @@
:class="{ 'dark-mode': isDarkMode, 'centered': centered }"
:key="showCreate ? 'create-mode' : 'normal-mode'"
>
<div v-if="selectedCount > 0" class="button selected-count-header">
<span>{{ selectedCount }} {{ $t("prompts.selected") }} </span>
<div class="context-menu-header">
<div
class="action button clickable"
v-if="!isSearchActive && userPerms.modify && !isShare"
@click="toggleShowCreate"
>
<i v-if="!showCreate" class="material-icons">add</i>
<i v-if="showCreate" class="material-icons">arrow_back</i>
</div>
<div
v-if="selectedCount > 0"
@mouseleave="hideTooltip"
@mouseenter="showTooltip($event, $t('buttons.selectedCount'))"
class="button selected-count-header"
>
<span>{{ selectedCount }}</span>
</div>
</div>
<action
v-if="!showCreate && !isSearchActive && userPerms.modify && !isShare"
icon="add"
:label="$t('buttons.new')"
@action="startShowCreate"
/>
<hr class="divider">
<action
v-if="showCreate && !isSearchActive && userPerms.modify"
icon="create_new_folder"
@@ -271,14 +281,6 @@ export default {
},
},
watch: {
currentPrompt: {
handler(prompt) {
if (prompt && prompt.name === "ContextMenu") {
this.setPositions();
}
},
deep: true,
},
hasOverflowItems: {
handler(hasItems) {
mutations.setContextMenuHasItems(hasItems);
@@ -306,6 +308,19 @@ export default {
}
},
methods: {
hideTooltip() {
mutations.hideTooltip();
},
showTooltip(event, text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
},
toggleShowCreate() {
this.showCreate = !this.showCreate;
},
shouldShowParentFolder() {
return this.isPreview && state.req.path != "/";
},
@@ -538,4 +553,10 @@ export default {
height: 0 !important;
opacity: 0;
}
.context-menu-header {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
+85 -23
View File
@@ -50,7 +50,7 @@
</select>
<!-- Formatted display of selected value -->
<div class="searchContext">{{ contextText }}</div>
<div class="searchContext">{{ $t("search.searchContext", { context: getContext }) }}</div>
</div>
<div id="result-list">
@@ -87,7 +87,7 @@
v-model="smallerThan"
type="number"
min="0"
placeholder="number"
:placeholder="$t('search.number')"
/><p>MB</p> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</div>
<div class="sizeInputWrapper">
@@ -96,7 +96,7 @@
class="sizeInput"
v-model="largerThan"
type="number"
placeholder="number"
:placeholder="$t('search.number')"
/><p>MB</p> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</div>
</div>
@@ -146,9 +146,8 @@ import { search } from "@/api";
import { getters, mutations, state } from "@/store";
import { getHumanReadableFilesize } from "@/utils/filesizes";
import { url } from "@/utils/";
import Icon from "@/components/files/Icon.vue";
import { serverHasMultipleSources, baseURL } from "@/utils/constants";
import { serverHasMultipleSources, baseURL, minSearchLength } from "@/utils/constants";
var boxes = {
folder: { label: "folders", icon: "folder" },
@@ -170,22 +169,22 @@ export default {
return {
largerThan: "",
smallerThan: "",
noneMessage: "Start typing 3 or more characters to begin searching.",
noneMessage: this.$t("search.typeToSearch", { minSearchLength: minSearchLength }),
searchTypes: "",
isTypeSelectDisabled: false,
showHelp: false,
folderSelect: [
{ label: "Only Folders", value: "type:folder" },
{ label: "Only Files", value: "type:file" },
{ label: this.$t("search.onlyFolders"), value: "type:folder" },
{ label: this.$t("search.onlyFiles"), value: "type:file" },
],
typeSelect: [
{ label: "Photos", value: "type:image" },
{ label: "Audio", value: "type:audio" },
{ label: "Videos", value: "type:video" },
{ label: "Documents", value: "type:doc" },
{ label: "Archives", value: "type:archive" },
{ label: this.$t("search.photos"), value: "type:image" },
{ label: this.$t("search.audio"), value: "type:audio" },
{ label: this.$t("search.videos"), value: "type:video" },
{ label: this.$t("search.documents"), value: "type:doc" },
{ label: this.$t("search.archives"), value: "type:archive" },
],
toggleOptionButton: [{ label: "Show Options" }],
toggleOptionButton: [{ label: this.$t("search.showOptions") }],
value: "",
ongoing: false,
results: [],
@@ -295,9 +294,7 @@ export default {
if (this.ongoing) {
return "";
}
return this.value === ""
? this.$t("search.typeToSearch")
: this.$t("search.pressToSearch");
return this.$t("search.typeToSearch", { minSearchLength: minSearchLength })
},
isRunning() {
return this.ongoing;
@@ -330,9 +327,6 @@ export default {
return "/"; // if searching on non-current source, search the whole thing
}
},
contextText() {
return `Search Context: ${this.getContext}`; // FIX: Use `this.getContext` as a property, not a function call
},
},
methods: {
openContext(event) {
@@ -489,9 +483,9 @@ export default {
if (event != undefined) {
event.preventDefault();
}
if (this.value === "" || this.value.length < 3) {
if (this.value === "" || this.value.length < minSearchLength) {
this.ongoing = false;
this.noneMessage = "Not enough characters to search (min 3)";
this.noneMessage = this.$t("search.notEnoughCharacters", { minSearchLength: minSearchLength });
return;
}
let searchTypesFull = this.searchTypes;
@@ -510,7 +504,7 @@ export default {
this.ongoing = false;
if (this.results.length == 0) {
this.noneMessage = "No results found in indexed search.";
this.noneMessage = this.$t("search.noResults");
}
},
toggleHelp() {
@@ -872,4 +866,72 @@ body.rtl #search .boxes h3 {
padding-right: 0.5em;
min-width: fit-content;
}
@media (max-width: 800px) {
#search {
min-width: unset;
max-width: 60%;
}
#search.active {
display: block;
position: fixed;
top: 0;
left: 50%;
width: 100%;
max-width: 100%;
}
#search-input {
transition: 1s ease all;
}
#search.active #search-input {
border-bottom: 3px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(6px);
height: 4em;
}
#search.active>div {
border-radius: 0 !important;
}
#search.active #result {
height: 100vh;
padding-top: 0;
}
#search.active #result>p>i {
text-align: center;
margin: 0 auto;
display: table;
}
#search.active #result ul li a {
display: flex;
align-items: center;
padding: .3em 0;
margin-right: .3em;
}
#search-input>.action,
#search-input>i {
margin-right: 0.3em;
user-select: none;
}
#result-list {
width:100vw !important;
max-width: 100vw !important;
left: 0;
top: 4em;
-webkit-box-direction: normal;
-ms-flex-direction: column;
overflow: scroll;
display: flex;
flex-direction: column;
}
}
</style>
+32 -4
View File
@@ -3,14 +3,14 @@
ref="tooltipRef"
v-show="tooltip.show"
class="floating-tooltip fb-shadow"
:class="{ 'dark-mode': isDarkMode }"
:class="{ 'dark-mode': isDarkMode, 'pointer-enabled': tooltip.pointerEvents }"
:style="tooltipStyle"
v-html="tooltip.content"
></div>
</template>
<script>
import { state } from "@/store";
import { state,getters } from "@/store";
export default {
name: "Tooltip",
@@ -26,13 +26,27 @@ export default {
return state.tooltip;
},
isDarkMode() {
return state.user.darkMode;
return getters.isDarkMode();
},
tooltipStyle() {
return {
const style = {
top: `${this.adjustedY}px`,
left: `${this.adjustedX}px`,
};
// Add custom width if specified
if (this.tooltip.width) {
style.maxWidth = this.tooltip.width;
style.width = this.tooltip.width;
}
// Add max height and scrolling for viewport overflow
if (this.tooltip.width || this.tooltip.pointerEvents) {
style.maxHeight = '80vh';
style.overflowY = 'auto';
}
return style;
},
},
watch: {
@@ -53,6 +67,10 @@ export default {
},
},
methods: {
/**
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
*/
updatePosition(x, y) {
this.$nextTick(() => {
const tooltipEl = this.$refs.tooltipRef;
@@ -101,6 +119,16 @@ export default {
white-space: normal;
overflow-wrap: break-word;
}
.floating-tooltip.pointer-enabled {
pointer-events: auto;
cursor: auto;
}
.floating-tooltip.pointer-enabled a {
pointer-events: auto;
cursor: pointer;
}
.tooltip-info-icon {
font-size: 1em !important;
padding: 0.1em !important;
+3 -3
View File
@@ -31,7 +31,7 @@ import {
baseURL,
} from "@/utils/constants";
import { getTypeInfo } from "@/utils/mimetype";
import { mutations, state } from "@/store";
import { mutations, state, getters } from "@/store";
import { shareInfo } from "@/utils/constants";
// NEW: Define placeholder and error image URLs for easy configuration
@@ -126,10 +126,10 @@ export default {
return this.imageTargetSrc;
},
showLargeIcon() {
return state.user.viewMode === "gallery" && state.user.preview.highQuality;
return getters.viewMode() === "gallery" && state.user.preview.highQuality;
},
showLarger() {
return state.user.viewMode === "gallery" || state.user.viewMode === "normal";
return getters.viewMode() === "gallery" || getters.viewMode() === "normal";
},
hasMotion() {
return (
+7 -10
View File
@@ -109,7 +109,7 @@ export default {
],
computed: {
galleryView() {
return state.user.viewMode === "gallery";
return getters.viewMode() === "gallery";
},
quickDownloadEnabled() {
// @ts-ignore
@@ -465,19 +465,16 @@ export default {
) {
mutations.resetSelected();
}
// @ts-ignore
mutations.addSelected(this.index);
mutations.setLastSelectedIndex(this.index);
},
open() {
if (this.hash) {
const shareHash = this.hash;
url.goToItem(this.source, this.path, "", shareHash);
return;
}
// @ts-ignore
const previousHash = state.req.items[this.index].name;
url.goToItem(this.source, this.path, previousHash);
const previousHistoryItem = {
name: state.req.items[this.index].name,
source: state.req.source,
path: state.req.path,
};
url.goToItem(this.source, this.path, previousHistoryItem);
},
},
};
+2 -3
View File
@@ -4,16 +4,15 @@
</div>
<div class="card-content">
<p>{{ $t("help.description") }} <a class="link" href="https://github.com/gtsteffaniak/filebrowser/wiki" target="_blank">{{ $t("help.wiki") }}</a></p>
<ul>
<li><strong>F1</strong> - {{ $t("help.f1") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<li><strong>F2</strong> - {{ $t("help.f2") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<li><strong>DEL</strong> - {{ $t("help.del") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<li><strong>ESC</strong> - {{ $t("help.esc") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<li><strong>CTRL + S</strong> - {{ $t("help.ctrl.s") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<li><strong>CTRL + F</strong> - {{ $t("help.ctrl.f") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<li><strong>CTRL + Click</strong> - {{ $t("help.ctrl.click") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<li><strong>Click</strong> - {{ $t("help.click") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<li><strong>Double click</strong> - {{ $t("help.doubleClick") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<li><strong>CTRL + D</strong> - {{ $t("help.ctrl.d") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</ul>
</div>
+50 -15
View File
@@ -4,11 +4,16 @@
</div>
<div class="card-content">
<p>{{ $t("prompts.renameMessage", { filename: oldName }) }}</p>
<input class="input" :class="{ 'form-invalid': !validateFileName(name) }" v-focus type="text"
@keyup.enter="submit" v-model.trim="name" />
<p v-if="!validateFileName(name) && name.length > 0" class="validation-error">
{{ $t("prompts.invalidName") }}
<p>{{ $t("prompts.renameMessage") }}</p>
<input class="input" :class="{ 'form-invalid': !validation.valid }" v-focus type="text" @keyup.enter="submit"
v-model.trim="name" />
<p v-if="!validation.valid && name.length > 0" class="validation-error">
<span v-if="validation.reason === 'conflict'">
{{ $t("prompts.renameMessageConflict", { filename: name }) }}
</span>
<span v-else>
{{ $t("prompts.renameMessageInvalid") }}
</span>
</p>
</div>
@@ -17,7 +22,7 @@
:title="$t('buttons.cancel')">
{{ $t("buttons.cancel") }}
</button>
<button @click="submit" class="button button--flat" :disabled="!validateFileName(name) || name.length === 0"
<button @click="submit" class="button button--flat" :disabled="!validation.valid || name.length === 0"
type="submit" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')">
{{ $t("buttons.rename") }}
</button>
@@ -25,7 +30,7 @@
</template>
<script>
import { filesApi } from "@/api";
import { mutations } from "@/store";
import { mutations, state } from "@/store";
import { notify } from "@/notify";
export default {
@@ -49,6 +54,9 @@ export default {
oldName() {
return this.item?.name || "";
},
validation() {
return this.validateFileName(this.name);
}
},
methods: {
/**
@@ -56,24 +64,51 @@ export default {
*/
validateFileName(value) {
if (value === "") {
return false;
return { valid: true };
}
// Check for forbidden characters: forward slash and backslash
const forbiddenChars = /[/\\]/;
return !forbiddenChars.test(value);
const isFolder = this.item.type === "directory";
// Check for forbidden characters in file names
if (!isFolder) {
const forbiddenChars = /[/\\]/;
if (forbiddenChars.test(value)) {
return { valid: false, reason: 'invalidChar' };
}
}
// Check if the item already exists
for (const item of state.req.items) {
if (item.name.toLowerCase() === value.toLowerCase()) {
if (isFolder === (item.type === "directory")) {
return { valid: false, reason: 'conflict' };
}
}
}
return { valid: true };
},
async submit() {
// Validate before submitting
if (!this.validateFileName(this.name) || this.name.length === 0) {
notify.showError(this.$t("prompts.invalidName"));
// remove trailing slashes
if (this.name.endsWith("/") || this.name.endsWith("\\")) {
this.name = this.name.substring(0, this.name.length - 1);
}
if (!this.validation.valid) {
return;
}
let newPath = this.item.path.substring(0, this.item.path.lastIndexOf("/"));
newPath = `${newPath}/${this.name}`;
try {
if (this.name === this.item.name) {
mutations.closeHovers();
return;
}
const items = [{
from: this.item.path,
fromSource: this.item.source,
to: this.item.path.replace(/[^/]+$/, this.name),
to: newPath,
toSource: this.item.source,
}];
+40 -81
View File
@@ -89,6 +89,25 @@
/>
<ToggleSwitch class="item" v-model="disableAnonymous" :name="$t('share.disableAnonymous')" :description="$t('share.disableAnonymousDescription')" />
<ToggleSwitch class="item" v-model="enableAllowedUsernames" :name="$t('share.enableAllowedUsernames')" :description="$t('share.enableAllowedUsernamesDescription')" />
<ToggleSwitch v-if="onlyOfficeAvailable" class="item" v-model="enableOnlyOffice" :name="$t('share.enableOnlyOffice')" :description="$t('share.enableOnlyOfficeDescription')" />
<ToggleSwitch v-if="onlyOfficeAvailable" class="item" v-model="enableOnlyOfficeEditing" :name="$t('share.enableOnlyOfficeEditing')" :description="$t('share.enableOnlyOfficeEditingDescription')" />
<p>
{{ $t("share.enforceDarkLightMode") }}
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.enforceDarkLightModeDescription'))"
@mouseleave="hideTooltip"
>
help
</i>
</p>
<select class="input" v-model="enforceDarkLightMode">
<option value="default">{{ $t("share.default") }}</option>
<option value="dark">{{ $t("share.dark") }}</option>
<option value="light">{{ $t("share.light") }}</option>
</select>
<div v-if="enableAllowedUsernames" class="item">
<input class="input" type="text" v-model.trim="allowedUsernames" :placeholder="$t('share.allowedUsernamesPlaceholder')" />
</div>
@@ -111,26 +130,7 @@
</option>
</select>
</div>
<div class="advanced-toggle">
<button
class="button button--flat button--block"
@click="showAdvanced = !showAdvanced"
:aria-expanded="showAdvanced ? 'true' : 'false'"
aria-controls="advanced-settings"
:aria-label="showAdvanced ? $t('buttons.showLess') : $t('buttons.showMore')"
:title="showAdvanced ? $t('buttons.showLess') : $t('buttons.showMore')"
>
{{ showAdvanced ? $t('buttons.showLess') : $t('buttons.showMore') }}
</button>
</div>
<transition
name="expand"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<div id="advanced-settings" v-show="showAdvanced">
<SettingsItem :title="$t('buttons.showMore')" :collapsable="true" :start-collapsed="true">
<div class="settings-items">
<ToggleSwitch class="item" v-model="keepAfterExpiration" :name="$t('share.keepAfterExpiration')" :description="$t('share.keepAfterExpirationDescription')" />
<ToggleSwitch class="item" v-model="disableThumbnails" :name="$t('share.disableThumbnails')" :description="$t('share.disableThumbnailsDescription')" />
@@ -222,8 +222,7 @@
</i>
</p>
<input class="input" type="text" v-model.trim="favicon" />
</div>
</transition>
</SettingsItem>
</div>
</div>
@@ -250,12 +249,13 @@
<script>
import { notify } from "@/notify";
import { state, getters, mutations } from "@/store";
import { publicApi } from "@/api";
import { publicApi, shareApi } from "@/api";
import Clipboard from "clipboard";
import { fromNow } from "@/utils/moment";
import { buildItemUrl, fixDownloadURL } from "@/utils/url";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
import { userSelectableThemes, externalUrl } from "@/utils/constants";
import SettingsItem from "@/components/settings/SettingsItem.vue";
import { userSelectableThemes, externalUrl, onlyOfficeUrl } from "@/utils/constants";
import { eventBus } from "@/store/eventBus";
//import ViewMode from "@/components/settings/ViewMode.vue";
@@ -263,6 +263,7 @@ export default {
name: "share",
components: {
ToggleSwitch,
SettingsItem,
//ViewMode,
},
props: {
@@ -310,15 +311,20 @@ export default {
title: "",
description: "",
favicon: "",
showAdvanced: false,
quickDownload: false,
disableNavButtons: false,
disableShareCard: false,
disableSidebar: false,
enforceDarkLightMode: "default",
enableOnlyOffice: false,
enableOnlyOfficeEditing: false,
//viewMode: "normal",
};
},
computed: {
onlyOfficeAvailable() {
return onlyOfficeUrl !== "";
},
availableThemes() {
return userSelectableThemes || {};
},
@@ -390,6 +396,9 @@ export default {
this.disableNavButtons = this.link.hideNavButtons || false;
this.disableShareCard = this.link.disableShareCard || false;
this.disableSidebar = this.link.disableSidebar || false;
this.enforceDarkLightMode = this.link.enforceDarkLightMode || "default";
this.enableOnlyOffice = this.link.enableOnlyOffice || false;
this.enableOnlyOfficeEditing = this.link.enableOnlyOfficeEditing || false;
//this.viewMode = this.link.viewMode || "normal";
}
},
@@ -401,7 +410,7 @@ export default {
}
try {
// get last element of the path
const links = await publicApi.get(this.item.path, this.item.source);
const links = await shareApi.get(this.item.path, this.item.source);
this.links = links;
} catch (err) {
notify.showError(err);
@@ -420,45 +429,6 @@ export default {
});
},
methods: {
/**
* @param {Element} el
*/
beforeEnter(el) {
const element = /** @type {HTMLElement} */ (el);
element.style.height = '0';
element.style.opacity = '0';
},
/**
* @param {Element} el
* @param {() => void} done
*/
enter(el, done) {
const element = /** @type {HTMLElement} */ (el);
element.style.transition = '';
element.style.height = '0';
element.style.opacity = '0';
void element.offsetHeight;
element.style.transition = 'height 0.3s, opacity 0.3s';
element.style.height = element.scrollHeight + 'px';
element.style.opacity = '1';
setTimeout(() => {
element.style.height = 'auto';
done();
}, 300);
},
/**
* @param {Element} el
* @param {() => void} done
*/
leave(el, done) {
const element = /** @type {HTMLElement} */ (el);
element.style.transition = 'height 0.3s, opacity 0.3s';
element.style.height = element.scrollHeight + 'px';
void element.offsetHeight;
element.style.height = '0';
element.style.opacity = '0';
setTimeout(done, 300);
},
/**
* @param {MouseEvent} event
* @param {string} text
@@ -507,12 +477,15 @@ export default {
hideNavButtons: this.disableNavButtons,
disableShareCard: this.disableShareCard,
disableSidebar: this.disableSidebar,
enforceDarkLightMode: this.enforceDarkLightMode,
enableOnlyOffice: this.enableOnlyOffice,
enableOnlyOfficeEditing: this.enableOnlyOfficeEditing,
};
if (this.isEditMode) {
payload.hash = this.link.hash;
}
const res = await publicApi.create(payload);
const res = await shareApi.create(payload);
if (!this.isEditMode) {
this.links.push(res);
@@ -539,7 +512,7 @@ export default {
async deleteLink(event, link) {
event.preventDefault();
try {
await publicApi.remove(link.hash);
await shareApi.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
// emit event to reload shares in settings view
eventBus.emit('sharesChanged');
@@ -599,9 +572,6 @@ export default {
<style scoped>
.advanced-toggle {
margin-top: 1em;
}
.setting-item {
display: flex;
justify-content: space-between;
@@ -619,15 +589,4 @@ export default {
.input {
height: auto;
}
.expand-enter-active,
.expand-leave-active {
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.expand-enter,
.expand-leave-to {
height: 0 !important;
opacity: 0;
}
</style>
@@ -0,0 +1,166 @@
<template>
<div class="settings-group">
<div class="settings-group-title button" :class="{ 'unclickable': !collapsable }"
@click="collapsable ? toggleCollapse() : null">
<h3>{{ title }}</h3>
<i v-if="collapsable" class="material-symbols-outlined collapse-icon" :class="{ 'rotated': !actuallyCollapsed }">
expand_more
</i>
</div>
<transition name="expand" @before-enter="beforeEnter" @enter="enter" @leave="leave">
<div v-show="!actuallyCollapsed" class="settings-content">
<slot></slot>
</div>
</transition>
</div>
</template>
<script>
export default {
name: 'SettingsItem',
props: {
title: {
type: String,
required: true
},
collapsable: {
type: Boolean,
default: false
},
startCollapsed: {
type: Boolean,
default: false
},
forceCollapsed: {
type: Boolean,
default: null
}
},
data() {
return {
isCollapsed: this.startCollapsed
}
},
computed: {
actuallyCollapsed() {
// If forceCollapsed is explicitly set, use that, otherwise use internal state
return this.forceCollapsed !== null ? this.forceCollapsed : this.isCollapsed;
}
},
watch: {
forceCollapsed(newVal) {
if (newVal !== null) {
this.isCollapsed = newVal;
}
}
},
methods: {
toggleCollapse() {
if (this.forceCollapsed !== null) {
// Emit event for parent to handle accordion logic
this.$emit('toggle', this.title);
} else {
// Handle internally as before
this.isCollapsed = !this.isCollapsed;
}
},
/**
* @param {Element} el
*/
beforeEnter(el) {
const element = /** @type {HTMLElement} */ (el);
element.style.height = '0';
element.style.opacity = '0';
},
/**
* @param {Element} el
* @param {() => void} done
*/
enter(el, done) {
const element = /** @type {HTMLElement} */ (el);
element.style.transition = '';
element.style.height = '0';
element.style.opacity = '0';
void element.offsetHeight;
element.style.transition = 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
element.style.height = element.scrollHeight + 'px';
element.style.opacity = '1';
setTimeout(() => {
element.style.height = 'auto';
done();
}, 300);
},
/**
* @param {Element} el
* @param {() => void} done
*/
leave(el, done) {
const element = /** @type {HTMLElement} */ (el);
element.style.transition = 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
element.style.height = element.scrollHeight + 'px';
void element.offsetHeight;
element.style.height = '0';
element.style.opacity = '0';
setTimeout(done, 300);
},
}
}
</script>
<style scoped>
.settings-group-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 1em;
background: var(--alt-background) !important;
color: var(--textPrimary) !important;
padding: 0.5em;
}
.settings-group-title h3 {
margin: 0;
flex: 1;
}
.collapse-icon {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: var(--textSecondary);
}
.collapse-icon.rotated {
transform: rotate(180deg);
}
.settings-content {
overflow: hidden;
margin-top: 0.5em;
}
.expand-enter-active,
.expand-leave-active {
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.expand-enter,
.expand-leave-to {
height: 0 !important;
opacity: 0;
}
.settings-group-title.unclickable {
cursor: default;
user-select: none;
transition: opacity 0.2s ease;
}
/* Prevent content height issues during animation */
.settings-content .input,
.settings-content .settings-items {
height: auto;
}
</style>
+11 -12
View File
@@ -3,7 +3,7 @@
<div class="card-wrapper user-card">
<div
v-if="settingsAllowed && user.username !== 'anonymous'"
@click="navigateTo('/settings#profile-main')"
@click="navigateTo('/settings','#profile-main')"
class="inner-card"
>
<button
@@ -53,6 +53,7 @@
<i class="material-icons">ads_click</i>
</div>
<div
v-if="darkModeTogglePossible"
class="clickable"
:class="{ active: user?.darkMode }"
@click="toggleDarkMode"
@@ -178,6 +179,7 @@ export default {
sourceInfo: () => state.sources.info,
activeSource: () => state.sources.current,
realtimeActive: () => state.realtimeActive,
darkModeTogglePossible: () => shareInfo.enforceDarkLightMode != "dark" && shareInfo.enforceDarkLightMode != "light",
},
watch: {
route() {
@@ -218,17 +220,13 @@ export default {
}
mutations.updateCurrentUser({ stickySidebar: !state.user.stickySidebar });
},
navigateTo(path) {
const hashIndex = path.indexOf("#");
if (hashIndex !== -1) {
// Extract the hash
const hash = path.substring(hashIndex);
// Remove the hash from the path
const cleanPath = path.substring(0, hashIndex);
this.$router.push({ path: cleanPath, hash: hash }, () => {});
} else {
this.$router.push({ path: path }, () => {});
}
navigateTo(path,hash) {
mutations.setPreviousHistoryItem({
name: state.req.name,
source: state.req.source,
path: state.req.path,
});
this.$router.push({ path: path, hash: hash });
mutations.closeHovers();
},
navigateToLogin() {
@@ -406,6 +404,7 @@ button.action {
.quick-toggles .active {
background-color: var(--primaryColor) !important;
border-radius: 10em;
color: white;
}
.inner-card {
+10 -1
View File
@@ -14,7 +14,16 @@
<div class="buffer"></div>
<div v-if="!isSettings" class="credits">
<span v-for="item in externalLinks" :key="item.title">
<a :href="item.url" target="_blank" :title="item.title">{{ item.text }}</a>
<a
v-if="item.url === 'help prompt'"
href="#"
@click.prevent="help"
:title="$t('sidebar.help')"
>{{ $t("sidebar.help") }}</a
>
<a v-else :href="item.url" target="_blank" :title="item.title">{{
item.text
}}</a>
</span>
<span v-if="name != ''">
<h4 style="margin: 0">{{ name }}</h4>
+1 -2
View File
@@ -1,10 +1,9 @@
:root {
--blue: #2196f3;
--dark-blue: #1E88E5;
--red: #F44336;
--red: #f44336;
--dark-red: #D32F2F;
--moon-grey: #f2f2f2;
--icon-red: #da4453;
--icon-orange: #f47750;
--icon-yellow: #fdbc4b;
-61
View File
@@ -50,68 +50,7 @@
body.rtl .dashboard .row {
margin-right: unset;
}
#search {
min-width: unset;
max-width: 60%;
}
#search.active {
display: block;
position: fixed;
top: 0;
left: 50%;
width: 100%;
max-width: 100%;
}
#search-input {
transition: 1s ease all;
}
#search.active #search-input {
border-bottom: 3px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(6px);
height: 4em;
}
#search.active>div {
border-radius: 0 !important;
}
#search.active #result {
height: 100vh;
padding-top: 0;
}
#search.active #result>p>i {
text-align: center;
margin: 0 auto;
display: table;
}
#search.active #result ul li a {
display: flex;
align-items: center;
padding: .3em 0;
margin-right: .3em;
}
#search-input>.action,
#search-input>i {
margin-right: 0.3em;
user-select: none;
}
#result-list {
width:100vw !important;
max-width: 100vw !important;
left: 0;
top: 4em;
-webkit-box-direction: normal;
-ms-flex-direction: column;
overflow: scroll;
display: flex;
flex-direction: column;
}
}
+200 -189
View File
@@ -8,52 +8,52 @@
@import "./listing.css";
@import "./dashboard.css";
@import "./login.css";
@import './mobile.css';
@import "./mobile.css";
.link {
color: var(--blue);
color: var(--blue);
}
#main .spinner {
display: block;
text-align: center;
line-height: 0;
padding: 1em 0;
display: block;
text-align: center;
line-height: 0;
padding: 1em 0;
}
#main .spinner > div {
width: .8em;
height: .8em;
margin: 0 .1em;
font-size: 1em;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 100%;
display: inline-block;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
width: 0.8em;
height: 0.8em;
margin: 0 0.1em;
font-size: 1em;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 100%;
display: inline-block;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#main .spinner .bounce1 {
animation-delay: -0.32s;
animation-delay: -0.32s;
}
#main .spinner .bounce2 {
animation-delay: -0.16s;
animation-delay: -0.16s;
}
.delayed {
animation: delayed linear 100ms;
animation: delayed linear 100ms;
}
@keyframes delayed {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* * * * * * * * * * * * * * * *
@@ -61,223 +61,222 @@
* * * * * * * * * * * * * * * */
.action {
display: inline-block;
cursor: pointer;
transition: 0.2s ease all;
border: 0;
margin: 0;
color: #546E7A;
border-radius: 50%;
background: transparent;
padding: 0;
box-shadow: none;
vertical-align: middle;
text-align: left;
position: relative;
display: inline-block;
cursor: pointer;
transition: 0.2s ease all;
border: 0;
margin: 0;
color: #546e7a;
border-radius: 50%;
background: transparent;
padding: 0;
box-shadow: none;
vertical-align: middle;
text-align: left;
position: relative;
}
.action.disabled {
opacity: 0.2;
cursor: not-allowed;
opacity: 0.2;
cursor: not-allowed;
}
.action i {
padding: 0.4em;
transition: .1s ease-in-out all;
border-radius: 50%;
padding: 0.4em;
transition: 0.1s ease-in-out all;
border-radius: 50%;
}
.action:not(:disabled):hover {
background-color: rgba(0, 0, 0, .1);
background-color: rgba(0, 0, 0, 0.1);
}
.action ul {
position: absolute;
top: 0;
color: #7d7d7d;
list-style: none;
margin: 0;
padding: 0;
flex-direction: column;
display: flex;
position: absolute;
top: 0;
color: #7d7d7d;
list-style: none;
margin: 0;
padding: 0;
flex-direction: column;
display: flex;
}
.action ul li {
line-height: 1;
padding: .7em;
transition: .1s ease background-color;
line-height: 1;
padding: 0.7em;
transition: 0.1s ease background-color;
}
.action ul li:hover {
background-color: rgba(0, 0, 0, 0.04);
background-color: rgba(0, 0, 0, 0.04);
}
.action .counter {
display: block;
position: absolute;
bottom: 0;
right: 0;
background: var(--primaryColor);
color: #fff;
border-radius: 50%;
font-size: .75em;
width: 1.8em;
height: 1.8em;
text-align: center;
line-height: 1.55em;
font-weight: bold;
display: block;
position: absolute;
bottom: 0;
right: 0;
background: var(--primaryColor);
color: #fff;
border-radius: 50%;
font-size: 0.75em;
width: 1.8em;
height: 1.8em;
text-align: center;
line-height: 1.55em;
font-weight: bold;
}
/* PREVIEWER */
#previewer {
background-color: rgba(0, 0, 0, 0.9);
width: 100%;
height: 100%;
z-index: 9999;
background-color: rgba(0, 0, 0, 0.9);
width: 100%;
height: 100%;
z-index: 9999;
}
#previewer header {
background: none;
color: #fff;
background: none;
color: #fff;
}
#previewer header > .action i {
color: #fff;
color: #fff;
}
@media (min-width: 800px) {
#previewer header #dropdown .action i {
color: #fff;
}
#previewer header #dropdown .action i {
color: #fff;
}
}
#previewer header .action:hover {
background-color: rgba(255, 255, 255, 0.3)
background-color: rgba(255, 255, 255, 0.3);
}
#previewer header .action span {
display: none;
display: none;
}
#previewer .preview {
text-align: center;
max-height: 100%;
max-width: 100%;
height: 100%;
text-align: center;
max-height: 100%;
max-width: 100%;
height: 100%;
}
#previewer .preview pre {
text-align: left;
overflow: auto;
text-align: left;
overflow: auto;
}
#previewer .preview pre,
#previewer .preview video,
#previewer .preview img {
max-height: 100%;
margin: 0;
max-height: 100%;
margin: 0;
}
#previewer .preview video {
height: 100%;
height: 100%;
}
#previewer .preview .info {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 1.5em;
color: #fff;
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 1.5em;
color: #fff;
}
#previewer .preview .info .title {
margin-bottom: 1em;
margin-bottom: 1em;
}
#previewer .preview .info .title i {
display: block;
margin-bottom: .1em;
font-size: 4em;
display: block;
margin-bottom: 0.1em;
font-size: 4em;
}
#previewer .preview .info .button {
display: inline-block;
display: inline-block;
}
#previewer .preview .info .button:hover {
background-color: rgba(255, 255, 255, 0.2)
background-color: rgba(255, 255, 255, 0.2);
}
#previewer .preview .info .button i {
display: block;
margin-bottom: 4px;
font-size: 1.3em;
display: block;
margin-bottom: 4px;
font-size: 1.3em;
}
#previewer .pdf {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
#previewer h2.message {
color: rgba(255, 255, 255, 0.5)
color: rgba(255, 255, 255, 0.5);
}
#previewer>button {
margin: 0;
position: fixed;
top: calc(50% + 1.85em);
transform: translateY(-50%);
background-color: rgba(80, 80, 80, .5);
color: white;
border-radius: 50%;
cursor: pointer;
border: 0;
margin: 0;
padding: 0;
transition: 0.2s ease all;
#previewer > button {
margin: 0;
position: fixed;
top: calc(50% + 1.85em);
transform: translateY(-50%);
background-color: rgba(80, 80, 80, 0.5);
color: white;
border-radius: 50%;
cursor: pointer;
border: 0;
margin: 0;
padding: 0;
transition: 0.2s ease all;
}
#previewer>button.hidden {
opacity: 0;
visibility: hidden;
#previewer > button.hidden {
opacity: 0;
visibility: hidden;
}
#previewer>button>i {
padding: 0.4em;
#previewer > button > i {
padding: 0.4em;
}
#previewer>button:last-of-type {
right: 0.5em;
#previewer > button:last-of-type {
right: 0.5em;
}
#previewer .spinner {
text-align: center;
position: fixed;
top: calc(50% + 1.85em);
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
position: fixed;
top: calc(50% + 1.85em);
left: 50%;
transform: translate(-50%, -50%);
}
#previewer .spinner > div {
width: 18px;
height: 18px;
background-color: white;
width: 18px;
height: 18px;
background-color: white;
}
/* EDITOR */
#editor-container {
display: flex;
flex-direction: column;
background-color: none;
height: 100%;
width: 100%;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
background-color: none;
height: 100%;
width: 100%;
overflow: hidden;
position: relative;
}
#editor-container #editor {
flex: 1;
flex: 1;
}
/* * * * * * * * * * * * * * * *
@@ -285,9 +284,9 @@
* * * * * * * * * * * * * * * */
@keyframes spin {
100% {
transform: rotate(360deg);
}
100% {
transform: rotate(360deg);
}
}
/* * * * * * * * * * * * * * * *
@@ -295,31 +294,31 @@
* * * * * * * * * * * * * * * */
.rules > div {
display: flex;
align-items: center;
margin: .5rem 0;
display: flex;
align-items: center;
margin: 0.5rem 0;
}
.rules input[type="checkbox"] {
margin-right: .2rem;
margin-right: 0.2rem;
}
.rules input[type="text"] {
border: 1px solid#ddd;
padding: .2rem;
border: 1px solid#ddd;
padding: 0.2rem;
}
.rules label {
margin-right: .5rem;
margin-right: 0.5rem;
}
.rules button {
margin-left: auto;
margin-left: auto;
}
.rules button.delete {
padding: .2rem .5rem;
margin-left: .5rem;
padding: 0.2rem 0.5rem;
margin-left: 0.5rem;
}
/* * * * * * * * * * * * * * * *
@@ -327,78 +326,90 @@
* * * * * * * * * * * * * * * */
body.rtl .card-content textarea {
direction: ltr;
text-align: left;
direction: ltr;
text-align: left;
}
body.rtl .card-content .small + input {
direction: ltr;
text-align: left;
direction: ltr;
text-align: left;
}
body.rtl .card.floating .card-content .file-list {
direction: ltr;
text-align: left;
direction: ltr;
text-align: left;
}
.fb-shadow {
box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%);
-webkit-box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%);
box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%);
-webkit-box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%);
}
.clickable {
cursor: pointer;
cursor: pointer;
}
.clickable:hover,
.plyr .plyr__control:hover,
button:hover,
.action:hover,
.listing-item.drag-hover {
box-shadow: inset 0 -3em 3em rgba(217, 217, 217, 0.211), 0 0 0 2px var(--alt-background) !important;
/* Adjust shadow values as needed */
transform: scale(1.02);
/* Slightly enlarges the element */
box-shadow:
inset 0 -3em 3em rgba(217, 217, 217, 0.211),
0 0 0 2px var(--alt-background) !important;
/* Adjust shadow values as needed */
transform: scale(1.02);
/* Slightly enlarges the element */
}
.flat-right {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.flat-left {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.form-grow {
flex-grow: 1;
width: inherit;
min-width: 1em;
flex-grow: 1;
width: inherit;
min-width: 1em;
}
.form-flex-group {
display: flex;
flex-direction: row;
align-items: center;
display: flex;
flex-direction: row;
align-items: center;
}
.form-flex-group > .form-compact {
min-height: 2em;
width: unset !important;
min-height: 2em;
width: unset !important;
}
.form-button {
height: auto !important
height: auto !important;
}
.form-invalid {
border-color: red !important;
border-color: red !important;
}
.form-compact {
padding: 0em;
padding: 0em;
}
.form-dropdown {
width: unset !important;
width: unset !important;
}
.divider {
border-color: var(--divider);
width: 90%;
}
.tooltip-close-button {
max-width: 4em !important;
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "اكتمل المسح",
"showMore": "اعرض المزيد",
"showLess": "اعرض أقل",
"openParentFolder": "فتح المجلد الأصل"
"openParentFolder": "فتح المجلد الأصل",
"selectedCount": "عدد العناصر المحددة لتنفيذ إجراء ما"
},
"download": {
"downloadFile": "Download File",
@@ -84,15 +85,15 @@
"click": "حدد الملف أو المجلد",
"ctrl": {
"click": "حدد أكثر من ملف او مجلد",
"f": "إبدأ البحث",
"s": "حمل الملف او المجلد في هذا المكان"
"d": "تنزيل عناصر مختارة"
},
"del": "حذف البيانات المحددة",
"doubleClick": "فتح المجلد او الملف",
"esc": "مسح التحديد وإغلاق النافذة المنبثقة",
"f1": "هذه المعلومات",
"f2": "إعادة تسمية الملف",
"help": "مساعدة"
"help": "مساعدة",
"f1": "إظهار موجه المساعدة",
"description": "يمكنك عرض خيارات التنقل الأساسية أدناه. للحصول على معلومات إضافية، يرجى زيارة",
"wiki": "ويكي متصفح الملفات الكمي"
},
"languages": {
"he": "עברית",
@@ -134,7 +135,6 @@
"prompts": {
"copy": "نسخ",
"copyMessage": "رجاء حدد المكان لنسخ ملفاتك فيه:",
"deleteMessageMultiple": "هل تريد بالتأكيد حذف {count} ملف؟",
"deleteMessageSingle": "هل تريد بالتأكيد حذف هذا الملف/المجلد؟",
"deleteTitle": "حذف الملفات",
"displayName": "الإسم:",
@@ -142,7 +142,6 @@
"downloadMessage": "حدد إمتداد الملف المراد تحميله.",
"error": "لقد حدث خطأ ما",
"fileInfo": "معلومات الملف",
"lastModified": "آخر تعديل{suffix}",
"move": "نقل",
"moveMessage": "إختر مكان جديد للملفات أو المجلدات المراد نقلها:",
"newArchetype": "إنشاء منشور من المنشور الأصلي. الملف سيتم انشاءه في مجلد المحتويات.",
@@ -150,10 +149,7 @@
"newDirMessage": "رجاء أدخل اسم المجلد الجديد.",
"newFile": "ملف جديد",
"newFileMessage": "رجاء ادخل اسم الملف الجديد.",
"numberDirs": "عدد المجلدات{suffix}",
"numberFiles": "عدد الملفات{suffix}",
"rename": "إعادة تسمية",
"renameMessage": "إدراج اسم جديد لـ",
"replace": "إستبدال",
"replaceMessage": "أحد الملفات التي تحاول رفعها يتعارض مع ملف موجود بنفس الإسم. هل تريد إستبدال الملف الموجود؟\n",
"schedule": "جدولة",
@@ -161,7 +157,6 @@
"show": "عرض",
"size": "الحجم",
"upload": "التحميل",
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
"deleteUserMessage": "هل أنت متأكد من رغبتك في حذف هذا المستخدم؟",
"filesSelected": "الملفات المحددة",
"optionalPassword": "كلمة مرور اختيارية",
@@ -172,10 +167,6 @@
"dragAndDrop": "سحب الملفات وإفلاتها هنا",
"completed": "مكتمل",
"conflictsDetected": "التعارضات المكتشفة",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "الاسم المقدم غير صالح: لا يمكن أن يحتوي على \"/\" أو \"\\\"",
"source": "Source{suffix}",
"downloadsLimit": "حد عدد التنزيلات",
"maxBandwidth": "أقصى نطاق ترددي للتنزيل (كيلو بايت/ثانية)",
"shareTheme": "سمة المشاركة",
@@ -186,22 +177,44 @@
"shareBanner": "مشاركة عنوان URL للشعار",
"shareFavicon": "مشاركة عنوان URL المفضل",
"optionalPasswordHelp": "إذا تم تعيينها، يجب على المستخدمين إدخال كلمة المرور هذه لعرض المحتوى المشترك.",
"viewMode": "وضع العرض"
"viewMode": "وضع العرض",
"renameMessage": "أدخل اسماً جديداً:",
"renameMessageInvalid": "لا يمكن أن يحتوي الملف على \"/\" أو \"\\\"",
"source": "المصدر{suffix}",
"deleteMessageMultiple": "هل تريد بالتأكيد حذف الملف (الملفات) {count} ؟",
"deleteMessageShare": "هل أنت متأكد أنك تريد حذف هذه المشاركة؟ {path} ?",
"lastModified": "آخر تعديل{suffix}",
"numberDirs": "عدد الدلائل{suffix}",
"numberFiles": "عدد الملفات{suffix}",
"renameMessageConflict": "عنصر باسم \"{filename}\" موجود بالفعل!",
"uploadSettingsChunked": "الحد الأقصى للتحميلات المتزامنة: {maxConcurrentUpload} ، حجم القطعة: {uploadChunkSizeMb} ميغابايت",
"uploadSettingsNoChunk": "الحد الأقصى للتحميلات المتزامنة: {maxConcurrentUpload} ، تم تعطيل التحميل المقطّع"
},
"search": {
"images": "الصور",
"music": "الموسيقى",
"pdf": "PDF",
"pressToSearch": "Press enter to search...",
"search": "البحث...",
"typeToSearch": "Type to search...",
"types": "الأنواع",
"video": "فيديوهات",
"path": "المسار:",
"smallerThan": "أصغر من:",
"largerThan": "أكبر من:",
"helpText1": "يحدث البحث على كل حرف تكتبه (3 أحرف كحد أدنى لمصطلحات البحث).",
"helpText2": "الفهرس: يستخدم البحث الفهرس الذي يتم تحديثه تلقائيًا على الفاصل الزمني الذي تم تكوينه (افتراضيًا: 5 دقائق). قد يؤدي البحث عندما يكون البرنامج قد بدأ للتو إلى نتائج غير مكتملة."
"helpText2": "الفهرس: يستخدم البحث الفهرس الذي يتم تحديثه تلقائيًا على الفاصل الزمني الذي تم تكوينه (افتراضيًا: 5 دقائق). قد يؤدي البحث عندما يكون البرنامج قد بدأ للتو إلى نتائج غير مكتملة.",
"noResults": "لم يتم العثور على نتائج في البحث المفهرس.",
"onlyFolders": "المجلدات فقط",
"onlyFiles": "الملفات فقط",
"showOptions": "عرض الخيارات",
"photos": "الصور",
"audio": "الصوت",
"videos": "مقاطع الفيديو",
"documents": "المستندات",
"archives": "المحفوظات",
"number": "العدد",
"searchContext": "سياق البحث: {context}",
"notEnoughCharacters": "أحرف غير كافية للبحث (الحد الأدنى {minSearchLength})",
"typeToSearch": "ابدأ الكتابة {minSearchLength} أو أكثر لبدء البحث."
},
"settings": {
"admin": "Admin",
@@ -214,10 +227,8 @@
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
"branding": "Branding",
"brandingDirectoryPath": "Branding directory path",
"brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
"changePassword": "تغيير كلمة المرور",
"commandRunner": "Command runner",
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "تم تحديث الأوامر",
"createUserDir": "Auto create user home dir while adding new user",
"customStylesheet": "ستايل مخصص",
@@ -300,7 +311,14 @@
"adminOptions": "خيارات المستخدم المسؤول",
"shareDuration": "تنتهي صلاحيتها",
"searchOptions": "خيارات البحث",
"downloads": "التنزيلات"
"downloads": "التنزيلات",
"systemAdmin": "النظام والإدارة",
"configViewer": "عارض التكوين",
"configViewerShowFull": "إظهار التكوين الكامل",
"configViewerShowComments": "إظهار التعليقات",
"configViewerLoadConfig": "تكوين التحميل",
"brandingHelp": "يمكنك تخصيص شكل ومظهر مثيل متصفح الملفات الخاص بك عن طريق تغيير اسمه، واستبدال الشعار، وإضافة أنماط مخصصة، وحتى تعطيل الروابط الخارجية إلى GitHub.\nلمزيد من المعلومات حول العلامة التجارية المخصصة، يرجى الاطلاع على {0}.",
"commandRunnerHelp": "هنا يمكنك تعيين الأوامر التي يتم تنفيذها في الأحداث المسماة. يجب عليك كتابة أمر واحد لكل سطر. سيكون متغيرا البيئة {0} و {1} متاحين، حيث {0} بالنسبة إلى {1}. لمزيد من المعلومات حول هذه الميزة ومتغيرات البيئة المتاحة، يرجى قراءة {2}."
},
"sidebar": {
"help": "مساعدة",
@@ -358,12 +376,21 @@
"hideNavButtons": "إخفاء أزرار التنقل",
"hideNavButtonsDescription": "قم بإخفاء أزرار التنقل على شريط التنقل في المشاركة لإضفاء مظهر بسيط.",
"viewModeDescription": "التخطيط الافتراضي للصفحة المشتركة.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "تم إرسال مشاركة إليك لعرضها أو تنزيلها.",
"disableShareCard": "تعطيل بطاقة المشاركة المعطلة",
"disableShareCardDescription": "قم بتعطيل بطاقة المشاركة على الصفحة المشتركة في الشريط الجانبي أو في أعلى الصفحة على الهاتف المحمول.",
"disableSidebar": "تعطيل الشريط الجانبي",
"disableSidebarDescription": "تعطيل الشريط الجانبي في الصفحة المشتركة."
"disableSidebarDescription": "تعطيل الشريط الجانبي في الصفحة المشتركة.",
"enforceDarkLightMode": "فرض وضع السمة",
"enforceDarkLightModeDescription": "فرض وضع سمة معينة (داكنة أو فاتحة) لهذه المشاركة، وتجاوز تفضيلات المستخدم.",
"default": "لا تفرض وضعاً",
"dark": "داكن",
"light": "خفيف",
"enableOnlyOffice": "تمكين عارض OnlyOffice فقط",
"enableOnlyOfficeDescription": "السماح بعرض ملفات المكتب باستخدام OnlyOffice في هذه المشاركة.",
"enableOnlyOfficeEditing": "تمكين التحرير في المكتب فقط",
"enableOnlyOfficeEditingDescription": "السماح بتحرير الملفات المكتبية باستخدام OnlyOffice في هذه الحصة.",
"titleDefault": "الملفات المشتركة - {title}"
},
"api": {
"title": "مفاتيح واجهة برمجة التطبيقات (API):",
@@ -447,8 +474,8 @@
"totalDenied": "مرفوض",
"totalAllowed": "المسموح به",
"all": "رفض الكل",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "يوضح هذا ما يحدث عندما يحاول المستخدم الوصول إلى مسار لا توجد فيه قواعد وصول محددة. تعني \"السماح\" أنه يمكن للمستخدمين الوصول بشكل افتراضي، وتعني \"الرفض\" أنه يتم حظر المستخدمين بشكل افتراضي ما لم يُسمح لهم صراحةً."
"defaultBehaviorDescription": "يوضح هذا ما يحدث عندما يحاول المستخدم الوصول إلى مسار لا توجد فيه قواعد وصول محددة. تعني \"السماح\" أنه يمكن للمستخدمين الوصول بشكل افتراضي، وتعني \"الرفض\" أنه يتم حظر المستخدمين بشكل افتراضي ما لم يُسمح لهم صراحةً.",
"defaultBehavior": "الإجراء الافتراضي{suffix}"
},
"fileLoading": {
"title": "التحميلات والتنزيلات",
@@ -506,7 +533,13 @@
"defaultThemeDescription": "افتراضي - السمة الافتراضية",
"customTheme": "اختر المظهر الخاص بك",
"showSelectMultiple": "إظهار تحديد متعدد في قائمة السياق على سطح المكتب",
"showSelectMultipleDescription": "بشكل افتراضي، لا تحتوي قائمة سياق سطح المكتب على خيار \"تحديد متعدد\" مثل الهاتف المحمول. يظهر هذا الخيار دائمًا في قائمة السياق."
"showSelectMultipleDescription": "بشكل افتراضي، لا تحتوي قائمة سياق سطح المكتب على خيار \"تحديد متعدد\" مثل الهاتف المحمول. يظهر هذا الخيار دائمًا في قائمة السياق.",
"defaultMediaPlayer": "استخدم مشغل الوسائط الأصلي في متصفحك",
"defaultMediaPlayerDescription": "استخدم مشغل الوسائط الأصلي في متصفحك لملفات الفيديو والصوت بدلاً من مشغل الوسائط \"vue-plyr\" المضمن في متصفح FileBrowser.",
"debugOfficeEditor": "تمكين وضع التصحيح فقط",
"debugOfficeEditorDescription": "إظهار نافذة منبثقة للتصحيح مع معلومات إضافية في محرر OnlyOffice.",
"fileViewerOptions": "خيارات عارض الملفات",
"themeAndLanguage": "الموضوع واللغة"
},
"colors": {
"red": "أحمر",
@@ -521,7 +554,11 @@
"saveFailed": "فشل حفظ الملف. يرجى المحاولة مرة أخرى.",
"saveAborted": "تم إجهاض الحفظ. اسم الملف غير مطابق للملف النشط.",
"saveDisabled": "حفظ الملفات المشتركة غير مدعوم.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "تم إجهاض عملية الحفظ. الملف النشط للتطبيق ({activeFile}) لا يتطابق مع الملف الذي تحاول حفظه ({tryingToSave}).",
"syncFailed": "فشل مزامنة الحالة مع مسار \"{filename}\" بعد 5 محاولات. إجهاض إعداد المحرر لمنع تلف البيانات."
},
"player": {
"LoopEnabled": "تم تمكين الحلقة",
"LoopDisabled": "حلقة معطلة"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Vymazání dokončeno",
"showMore": "Zobrazit více",
"showLess": "Zobrazit méně",
"openParentFolder": "Otevření nadřazené složky"
"openParentFolder": "Otevření nadřazené složky",
"selectedCount": "Počet položek vybraných k provedení akce"
},
"download": {
"downloadFile": "Stáhnout soubor",
@@ -87,15 +88,15 @@
"click": "označit soubor nebo složku",
"ctrl": {
"click": "označit více souborů nebo složek",
"f": "otevře hledání",
"s": "uložit soubor nebo stáhnout aktuální složku"
"d": "stáhnout vybrané položky"
},
"del": "smazat označené položky",
"doubleClick": "otevřít soubor nebo složku",
"esc": "zrušit označení a/nebo zavřít dialog",
"f1": "tato nápověda",
"f2": "přejmenovat soubor",
"help": "Nápověda"
"help": "Nápověda",
"f1": "zobrazit výzvu nápovědy",
"description": "Základní možnosti navigace si můžete prohlédnout níže. Další informace naleznete na adrese",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -161,19 +162,26 @@
"hideNavButtons": "Skrytí navigačních tlačítek",
"hideNavButtonsDescription": "Skryjte navigační tlačítka na panelu navigace ve sdíleném okně, abyste vytvořili minimalistický vzhled.",
"viewModeDescription": "Výchozí rozložení sdílené stránky.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Byl vám zaslán podíl, který si můžete prohlédnout nebo stáhnout.",
"disableShareCard": "Zakázat sdílenou kartu",
"disableShareCardDescription": "Zakázat kartu sdílení na sdílené stránce v postranním panelu nebo v horní části stránky na mobilním telefonu.",
"disableSidebar": "Zakázat postranní panel",
"disableSidebarDescription": "Zakázat postranní panel na sdílené stránce."
"disableSidebarDescription": "Zakázat postranní panel na sdílené stránce.",
"enforceDarkLightMode": "Vynucení režimu tématu",
"enforceDarkLightModeDescription": "Vynucení určitého režimu motivu (tmavého nebo světlého) pro tuto sdílenou složku, který je nadřazen uživatelským preferencím.",
"default": "Nevynucujte režim",
"dark": "Dark",
"light": "Světlo",
"enableOnlyOffice": "Povolení prohlížeče OnlyOffice",
"enableOnlyOfficeDescription": "Povolit prohlížení kancelářských souborů pomocí OnlyOffice v této sdílené složce.",
"enableOnlyOfficeEditing": "Povolení úprav OnlyOffice",
"enableOnlyOfficeEditingDescription": "Povolit úpravy kancelářských souborů pomocí OnlyOffice v této sdílené složce.",
"titleDefault": "Sdílené soubory - {title}"
},
"prompts": {
"copy": "Kopírovat",
"copyMessage": "Vyberte místo, kam chcete soubory kopírovat:",
"deleteMessageMultiple": "Opravdu chcete smazat {count} soubor(ů)?",
"deleteMessageSingle": "Opravdu chcete smazat tento soubor/složku?",
"deleteMessageShare": "Opravdu chcete smazat toto sdílení: {path} ?",
"deleteUserMessage": "Opravdu chcete tohoto uživatele smazat?",
"deleteTitle": "Smazat soubory",
"displayName": "Zobrazované jméno:",
@@ -183,7 +191,6 @@
"fileInfo": "Informace o souboru",
"selected": "označeno",
"filesSelected": "souborů vybráno",
"lastModified": "Poslední změna",
"move": "Přesunout",
"moveMessage": "Vyberte nové umístění pro soubor(y)/složku(y):",
"newArchetype": "Vytvořit nový příspěvek podle archetypu. Váš soubor bude vytvořen ve složce content.",
@@ -191,10 +198,7 @@
"newDirMessage": "Zadejte název nové složky.",
"newFile": "Nový soubor",
"newFileMessage": "Zadejte název nového souboru.",
"numberDirs": "Počet složek{suffix}",
"numberFiles": "Počet souborů{suffix}",
"rename": "Přejmenovat",
"renameMessage": "Zadejte nový název pro",
"replace": "Nahradit",
"replaceMessage": "Jeden ze souborů, které se snažíte nahrát, má konflikt kvůli stejnému názvu. Chcete pokračovat v nahrávání nebo nahradit existující?",
"schedule": "Naplánovat",
@@ -209,10 +213,6 @@
"dragAndDrop": "Přetáhněte sem soubory",
"completed": "Dokončeno",
"conflictsDetected": "Zjištěné konflikty",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Nesprávný zadaný název: nelze použít kontejner \"/\" nebo \"\\\".",
"source": "Source{suffix}",
"downloadsLimit": "Limit počtu stažení",
"maxBandwidth": "Maximální šířka pásma stahování (kbps)",
"shareTheme": "Sdílet téma",
@@ -223,22 +223,44 @@
"shareBanner": "Sdílet adresu URL banneru",
"shareFavicon": "Sdílet adresu URL favicon",
"optionalPasswordHelp": "Pokud je nastaveno, musí uživatelé toto heslo zadat, aby mohli sdílený obsah zobrazit.",
"viewMode": "Režim zobrazení"
"viewMode": "Režim zobrazení",
"renameMessage": "Zadejte nový název:",
"renameMessageInvalid": "Soubor nemůže obsahovat \"/\" nebo \"\\\".",
"source": "Zdroj:{suffix}",
"deleteMessageMultiple": "Opravdu chcete odstranit soubor(y) {count}?",
"deleteMessageShare": "Jste si jisti, že chcete tuto sdílenou složku odstranit: {path} ?",
"lastModified": "Poslední úprava{suffix}",
"numberDirs": "Počet adresářů{suffix}",
"numberFiles": "Počet souborů{suffix}",
"renameMessageConflict": "Položka s názvem \"{filename}\" již existuje!",
"uploadSettingsChunked": "Maximální souběžné nahrávání: {maxConcurrentUpload}, velikost chunků: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Maximální souběžné nahrávání: {maxConcurrentUpload}, vypnuto hromadné nahrávání"
},
"search": {
"images": "Obrázky",
"music": "Hudba",
"pdf": "PDF",
"pressToSearch": "Ve vyhledávání s indexem nebyly nalezeny žádné výsledky.",
"search": "Hledat...",
"typeToSearch": "Začněte psát pro hledání... (minimálně 3 znaky)",
"path": "Cesta:",
"types": "Typy",
"video": "Video",
"smallerThan": "Menší než:",
"largerThan": "Větší než:",
"helpText1": "Vyhledávání probíhá po každém napsaném znaku (minimálně 3 znaky pro hledaný výraz).",
"helpText2": "Index: Vyhledávání využívá index, který se automaticky aktualizuje v nastaveném intervalu (výchozí: 5 minut). Hledání ihned po spuštění programu může vracet neúplné výsledky."
"helpText2": "Index: Vyhledávání využívá index, který se automaticky aktualizuje v nastaveném intervalu (výchozí: 5 minut). Hledání ihned po spuštění programu může vracet neúplné výsledky.",
"noResults": "V indexovaném vyhledávání nebyly nalezeny žádné výsledky.",
"onlyFolders": "Pouze složky",
"onlyFiles": "Pouze soubory",
"showOptions": "Zobrazit možnosti",
"photos": "Fotografie",
"audio": "Audio",
"videos": "Videa",
"documents": "Dokumenty",
"archives": "Archivy",
"number": "Číslo",
"searchContext": "Kontext vyhledávání: {context}",
"notEnoughCharacters": "Nedostatek znaků pro vyhledávání (min. {minSearchLength})",
"typeToSearch": "Pro zahájení vyhledávání začněte psát {minSearchLength} nebo více znaků."
},
"profileSettings": {
"setDateFormat": "Nastavit formát data",
@@ -289,7 +311,13 @@
"defaultThemeDescription": "default - Výchozí téma",
"customTheme": "Vyberte si téma",
"showSelectMultiple": "Zobrazení výběru více položek v kontextové nabídce na ploše",
"showSelectMultipleDescription": "Ve výchozím nastavení nemá kontextová nabídka na ploše možnost \"vybrat více\" jako v mobilním telefonu. Tato možnost se v kontextové nabídce zobrazuje vždy."
"showSelectMultipleDescription": "Ve výchozím nastavení nemá kontextová nabídka na ploše možnost \"vybrat více\" jako v mobilním telefonu. Tato možnost se v kontextové nabídce zobrazuje vždy.",
"defaultMediaPlayer": "Použití nativního přehrávače médií v prohlížeči",
"defaultMediaPlayerDescription": "Pro video a zvukové soubory používejte nativní přehrávač médií prohlížeče, nikoli přehrávač médií vue-plyr, který je součástí aplikace FileBrowser.",
"debugOfficeEditor": "Povolení režimu ladění OnlyOffice",
"debugOfficeEditorDescription": "Zobrazení vyskakovacího okna pro ladění s dalšími informacemi v editoru OnlyOffice.",
"fileViewerOptions": "Možnosti prohlížeče souborů",
"themeAndLanguage": "Téma a jazyk"
},
"settings": {
"enterPassword": "Zadejte heslo",
@@ -316,10 +344,8 @@
"avoidChanges": "Zaškrtněte také 'změnit heslo' pro použití tohoto hesla",
"branding": "Branding",
"brandingDirectoryPath": "Cesta k adresáři s brandingem",
"brandingHelp": "Můžete si upravit vzhled File Browseru změnou názvu, loga, přidáním vlastních stylů nebo zakázáním externích odkazů na GitHub.\nPro více informací o úpravách vzhledu navštivte {0}.",
"changePassword": "Změnit heslo",
"commandRunner": "Spouštěč příkazů",
"commandRunnerHelp": "Zde můžete nastavit příkazy, které se budou spouštět při vybraných událostech. Každý příkaz pište na nový řádek. Proměnné prostředí {0} a {1} budou dostupné; {0} je relativní k {1}. Pro více informací o této funkci a dostupných proměnných si přečtěte {2}.",
"commandsUpdated": "Příkazy aktualizovány!",
"createUserDir": "Automaticky vytvářet domovský adresář uživatele při přidání nového uživatele",
"tusUploads": "Dávkové nahrávání",
@@ -388,7 +414,14 @@
"username": "Uživatelské jméno",
"users": "Uživatelé",
"adminOptions": "Možnosti administrátora",
"downloads": "Ke stažení na"
"downloads": "Ke stažení na",
"systemAdmin": "Systém a správce",
"configViewer": "Prohlížeč konfigurace",
"configViewerShowFull": "Zobrazit úplnou konfiguraci",
"configViewerShowComments": "Zobrazit komentáře",
"configViewerLoadConfig": "Konfigurace načítání",
"brandingHelp": "Vzhled instance Průzkumníka souborů si můžete přizpůsobit změnou názvu, nahrazením loga, přidáním vlastních stylů a dokonce i zakázáním externích odkazů na GitHub.\nDalší informace o vlastním brandingu naleznete na stránce {0}.",
"commandRunnerHelp": "Zde můžete nastavit příkazy, které se provedou v pojmenovaných událostech. Na každý řádek musíte napsat jeden. K dispozici budou proměnné prostředí {0} a {1}, přičemž {0} bude relativní vůči {1}. Další informace o této funkci a dostupných proměnných prostředí naleznete na {2}."
},
"sidebar": {
"help": "Nápověda",
@@ -513,15 +546,19 @@
"name": "Název",
"accessManagement": "Správa přístupu",
"all": "Odmítnout vše",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "To ukazuje, co se stane, když se uživatel pokusí získat přístup k cestě, kde neexistují žádná specifická pravidla přístupu. \"Povolit\" znamená, že uživatelé mají ve výchozím nastavení přístup, \"Odmítnout\" znamená, že uživatelé jsou ve výchozím nastavení blokováni, pokud není výslovně povoleno."
"defaultBehaviorDescription": "To ukazuje, co se stane, když se uživatel pokusí získat přístup k cestě, kde neexistují žádná specifická pravidla přístupu. \"Povolit\" znamená, že uživatelé mají ve výchozím nastavení přístup, \"Odmítnout\" znamená, že uživatelé jsou ve výchozím nastavení blokováni, pokud není výslovně povoleno.",
"defaultBehavior": "Výchozí akce{suffix}"
},
"editor": {
"uninitialized": "Nepodařilo se inicializovat editor. Prosím, načtěte jej znovu.",
"saveFailed": "Soubor se nepodařilo uložit. Zkuste to prosím znovu.",
"saveAborted": "Uložení přerušeno. Název souboru neodpovídá aktivnímu souboru.",
"saveDisabled": "Ukládání sdílených souborů není podporováno.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Operace ukládání byla přerušena. Aktivní soubor aplikace ({activeFile}) neodpovídá souboru, který se pokoušíte uložit ({tryingToSave}).",
"syncFailed": "po 5 pokusech se nepodařilo synchronizovat stav s trasou pro \"{filename}\". Přerušení nastavení editoru, aby se zabránilo poškození dat."
},
"player": {
"LoopEnabled": "Povolená smyčka",
"LoopDisabled": "Vypnutá smyčka"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Abgeschlossene löschen",
"showMore": "Mehr anzeigen",
"showLess": "Weniger anzeigen",
"openParentFolder": "Übergeordneten Ordner öffnen"
"openParentFolder": "Übergeordneten Ordner öffnen",
"selectedCount": "Anzahl der zur Durchführung einer Aktion ausgewählten Elemente"
},
"download": {
"downloadFile": "Datei herunterladen",
@@ -84,15 +85,15 @@
"click": "Wähle Datei oder Ordner",
"ctrl": {
"click": "Markiere mehrere Dateien oder Ordner",
"f": "Öffnet eine neue Suche",
"s": "Speichert eine Datei oder einen Ordner am akutellen Ort"
"d": "ausgewählte Artikel herunterladen"
},
"del": "Löscht die ausgewählten Elemente",
"doubleClick": "Öffnet eine Datei oder einen Ordner",
"esc": "Auswahl zurücksetzen und/oder Dialog schließen",
"f1": "Diese Informationsseite",
"f2": "Datei umbenennen",
"help": "Hilfe"
"help": "Hilfe",
"f1": "Hilfeprompt anzeigen",
"description": "Nachstehend finden Sie die grundlegenden Navigationsoptionen. Für zusätzliche Informationen besuchen Sie bitte",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -134,16 +135,13 @@
"prompts": {
"copy": "Kopieren",
"copyMessage": "Wählen Sie einen Zielort zum Kopieren:",
"deleteMessageMultiple": "Sind Sie sicher, dass Sie {count} Datei(en) löschen möchten?",
"deleteMessageSingle": "Sind Sie sicher, dass Sie diesen Ordner/diese Datei löschen möchten?",
"deleteMessageShare": "Sind Sie sicher, dass Sie diese Freigabe löschen möchten ({path})?",
"deleteTitle": "Dateien löschen",
"displayName": "Anzeigename:",
"download": "Dateien herunterladen",
"downloadMessage": "Wählen Sie ein Format zum Herunterladen aus.",
"error": "Etwas ist schief gelaufen",
"fileInfo": "Dateiinformation",
"lastModified": "Zuletzt geändert{suffix}",
"move": "Verschieben",
"moveMessage": "Wählen Sie einen neuen Ort für ihre Datei(en)/Ordner:",
"newArchetype": "Erstelle neuen Beitrag auf Basis eines Archetyps. Ihre Datei wird im Inhalteordner erstellt.",
@@ -151,10 +149,7 @@
"newDirMessage": "Geben Sie den Namen des neuen Ordners an.",
"newFile": "Neue Datei",
"newFileMessage": "Geben Sie den Namen der neuen Datei an.",
"numberDirs": "Anzahl der Ordner{suffix}",
"numberFiles": "Anzahl der Dateien{suffix}",
"rename": "Umbenennen",
"renameMessage": "Geben Sie einen Namen ein für",
"replace": "Ersetzen",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei übersprungen oder ersetzt werden?\n",
"schedule": "Zeitplan",
@@ -172,10 +167,6 @@
"dragAndDrop": "Dateien hierher ziehen und ablegen",
"completed": "Abgeschlossen",
"conflictsDetected": "Erkannte Widersprüche",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Ungültiger Name angegeben: kann nicht Container \"/\" oder \"\\\"",
"source": "Source{suffix}",
"downloadsLimit": "Limit für die Anzahl der Downloads",
"maxBandwidth": "Maximale Download-Bandbreite (kbps)",
"shareTheme": "Thema teilen",
@@ -186,22 +177,44 @@
"shareBanner": "Banner-URL freigeben",
"shareFavicon": "Favicon-URL freigeben",
"optionalPasswordHelp": "Falls festgelegt, müssen die Benutzer dieses Kennwort eingeben, um die freigegebenen Inhalte anzuzeigen.",
"viewMode": "Ansichtsmodus"
"viewMode": "Ansichtsmodus",
"renameMessage": "Geben Sie einen neuen Namen ein:",
"renameMessageInvalid": "Eine Datei kann nicht den Container \"/\" oder \"\\\" enthalten.",
"source": "Quelle{suffix}",
"deleteMessageMultiple": "Sind Sie sicher, dass Sie die Datei(en) {count} löschen möchten?",
"deleteMessageShare": "Sind Sie sicher, dass Sie diese Freigabe löschen möchten: {path} ?",
"lastModified": "Zuletzt modifiziert{suffix}",
"numberDirs": "Anzahl der Verzeichnisse{suffix}",
"numberFiles": "Anzahl der Dateien{suffix}",
"renameMessageConflict": "Ein Artikel mit dem Namen \"{filename}\" existiert bereits!",
"uploadSettingsChunked": "Max. gleichzeitige Uploads: {maxConcurrentUpload}, Chunk-Größe: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max. gleichzeitige Uploads: {maxConcurrentUpload}, Chunked Upload deaktiviert"
},
"search": {
"images": "Bilder",
"music": "Musik",
"pdf": "PDF",
"pressToSearch": "Drücken Sie Enter, um zu suchen...",
"search": "Suche...",
"typeToSearch": "Tippen, um zu suchen...",
"types": "Typen",
"video": "Video",
"path": "Pfad:",
"smallerThan": "Kleiner als:",
"largerThan": "Größer als:",
"helpText1": "Die Suche erfolgt bei jedem Zeichen, das Sie eingeben (mindestens 3 Zeichen für Suchbegriffe).",
"helpText2": "Der Index: Die Suche verwendet den Index, der automatisch in dem konfigurierten Intervall aktualisiert wird (Standard: 5 Minuten). Eine Suche direkt nach dem Start des Programms kann zu unvollständigen Ergebnissen führen."
"helpText2": "Der Index: Die Suche verwendet den Index, der automatisch in dem konfigurierten Intervall aktualisiert wird (Standard: 5 Minuten). Eine Suche direkt nach dem Start des Programms kann zu unvollständigen Ergebnissen führen.",
"noResults": "Keine Ergebnisse in der indizierten Suche gefunden.",
"onlyFolders": "Nur Ordner",
"onlyFiles": "Nur Dateien",
"showOptions": "Optionen anzeigen",
"photos": "Fotos",
"audio": "Audio",
"videos": "Videos",
"documents": "Dokumente",
"archives": "Archiv",
"number": "Nummer",
"searchContext": "Suche im Kontext: {context}",
"notEnoughCharacters": "Nicht genügend Zeichen für die Suche (min {minSearchLength})",
"typeToSearch": "Beginnen Sie mit der Eingabe von {minSearchLength} oder mehr Zeichen, um die Suche zu starten."
},
"settings": {
"admin": "Admin",
@@ -214,10 +227,8 @@
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
"branding": "Branding",
"brandingDirectoryPath": "Branding-Verzeichnispfad",
"brandingHelp": "Sie können das Erscheinungsbild Ihres FileBrowser anpassen, indem Sie den Namen ändern, das Logo austauschen oder eigene Branding definieren bzw. sogar externe Links zu GitHub deaktivieren.\nUm mehr Informationen zum Anpassen des Designs zu bekommen, gehen Sie bitte zu {0}.",
"changePassword": "Kennwort ändern",
"commandRunner": "Befehlseingabe",
"commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariablen {0} und {1} sind verfügbar, wobei {0} relativ zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen lesen Sie bitte die {2}.",
"commandsUpdated": "Befehle aktualisiert!",
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
"tusUploads": "Gestückeltes Hochladen",
@@ -300,7 +311,14 @@
"adminOptions": "Optionen für Verwaltungsbenutzer",
"shareDuration": "Abgelaufen",
"searchOptions": "Suchoptionen",
"downloads": "Herunterladen"
"downloads": "Herunterladen",
"systemAdmin": "System & Verwaltung",
"configViewer": "Config Viewer",
"configViewerShowFull": "Vollständige Konfiguration anzeigen",
"configViewerShowComments": "Kommentare anzeigen",
"configViewerLoadConfig": "Lastkonfiguration",
"brandingHelp": "Sie können das Aussehen Ihrer Dateibrowser-Instanz anpassen, indem Sie den Namen ändern, das Logo ersetzen, eigene Stile hinzufügen und sogar externe Links zu GitHub deaktivieren.\nWeitere Informationen zum benutzerdefinierten Branding finden Sie unter {0}.",
"commandRunnerHelp": "Hier können Sie Befehle eingeben, die bei den genannten Ereignissen ausgeführt werden. Sie müssen einen pro Zeile schreiben. Die Umgebungsvariablen {0} und {1} werden verfügbar sein, wobei {0} relativ zu {1} ist. Weitere Informationen über diese Funktion und die verfügbaren Umgebungsvariablen finden Sie unter {2}."
},
"sidebar": {
"help": "Hilfe",
@@ -358,12 +376,21 @@
"hideNavButtons": "Navigationsschaltflächen ausblenden",
"hideNavButtonsDescription": "Blenden Sie die Navigationsschaltflächen auf der Navigationsleiste in der Freigabe aus, um ein minimalistisches Aussehen zu erzielen.",
"viewModeDescription": "Standardlayout für die gemeinsame Seite.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Sie haben eine Aktie zur Ansicht oder zum Download erhalten.",
"disableShareCard": "Freigabekarte deaktivieren",
"disableShareCardDescription": "Deaktivieren Sie die Freigabekarte auf der freigegebenen Seite in der Seitenleiste oder oben auf der Seite auf dem Handy.",
"disableSidebar": "Seitenleiste deaktivieren",
"disableSidebarDescription": "Deaktivieren Sie die Seitenleiste auf der gemeinsamen Seite."
"disableSidebarDescription": "Deaktivieren Sie die Seitenleiste auf der gemeinsamen Seite.",
"enforceDarkLightMode": "Themenmodus erzwingen",
"enforceDarkLightModeDescription": "Erzwingt einen bestimmten Themenmodus (dunkel oder hell) für diese Freigabe und setzt die Benutzereinstellungen außer Kraft.",
"default": "Keinen Modus erzwingen",
"dark": "Dunkelheit",
"light": "Licht",
"enableOnlyOffice": "OnlyOffice viewer aktivieren",
"enableOnlyOfficeDescription": "Erlauben Sie die Anzeige von Office-Dateien mit OnlyOffice in dieser Freigabe.",
"enableOnlyOfficeEditing": "OnlyOffice-Bearbeitung aktivieren",
"enableOnlyOfficeEditingDescription": "Erlauben Sie die Bearbeitung von Office-Dateien mit OnlyOffice in dieser Freigabe.",
"titleDefault": "Gemeinsame Dateien - {title}"
},
"api": {
"title": "API-Schlüssel:",
@@ -447,8 +474,8 @@
"totalDenied": "Verweigert",
"totalAllowed": "Erlaubt",
"all": "Alle ablehnen",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Dies zeigt, was passiert, wenn ein Benutzer versucht, auf einen Pfad zuzugreifen, für den keine spezifischen Zugriffsregeln bestehen. Zulassen\" bedeutet, dass Benutzer standardmäßig Zugang haben, \"Verweigern\" bedeutet, dass Benutzer standardmäßig blockiert werden, wenn sie nicht ausdrücklich zugelassen sind."
"defaultBehaviorDescription": "Dies zeigt, was passiert, wenn ein Benutzer versucht, auf einen Pfad zuzugreifen, für den keine spezifischen Zugriffsregeln bestehen. Zulassen\" bedeutet, dass Benutzer standardmäßig Zugang haben, \"Verweigern\" bedeutet, dass Benutzer standardmäßig blockiert werden, wenn sie nicht ausdrücklich zugelassen sind.",
"defaultBehavior": "Standard-Aktion{suffix}"
},
"fileLoading": {
"title": "Uploads & Downloads",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - Das Standardthema",
"customTheme": "Wählen Sie Ihr Thema",
"showSelectMultiple": "Mehrfachauswahl im Kontextmenü auf dem Desktop anzeigen",
"showSelectMultipleDescription": "Standardmäßig verfügt das Desktop-Kontextmenü nicht über die Option \"Mehrere auswählen\" wie bei mobilen Geräten. Diese Option wird immer im Kontextmenü angezeigt."
"showSelectMultipleDescription": "Standardmäßig verfügt das Desktop-Kontextmenü nicht über die Option \"Mehrere auswählen\" wie bei mobilen Geräten. Diese Option wird immer im Kontextmenü angezeigt.",
"defaultMediaPlayer": "Verwenden Sie den nativen Media Player Ihres Browsers",
"defaultMediaPlayerDescription": "Verwenden Sie für Video- und Audiodateien den nativen Mediaplayer Ihres Browsers und nicht den in FileBrowser enthaltenen 'vue-plyr' Mediaplayer.",
"debugOfficeEditor": "OnlyOffice-Debug-Modus einschalten",
"debugOfficeEditorDescription": "Ein Debug-Popup mit zusätzlichen Informationen im OnlyOffice-Editor anzeigen.",
"fileViewerOptions": "Optionen des Dateibetrachters",
"themeAndLanguage": "Thema und Sprache"
},
"editor": {
"uninitialized": "Der Editor konnte nicht initialisiert werden. Bitte neu laden.",
"saveFailed": "Datei konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.",
"saveAborted": "Speichern abgebrochen. Der Dateiname stimmt nicht mit der aktiven Datei überein.",
"saveDisabled": "Das Speichern von gemeinsam genutzten Dateien wird nicht unterstützt.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Der Speichervorgang wurde abgebrochen. Die aktive Datei der Anwendung ({activeFile}) stimmt nicht mit der Datei überein, die Sie zu speichern versuchen ({tryingToSave}).",
"syncFailed": "Nach 5 Versuchen ist es nicht gelungen, den Status mit der Route für \"{filename}\" zu synchronisieren. Abbruch der Editor-Einrichtung, um Datenbeschädigung zu vermeiden."
},
"player": {
"LoopEnabled": "Schleife Aktiviert",
"LoopDisabled": "Schleife Deaktiviert"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Εκκαθάριση ολοκληρώθηκε",
"showMore": "Εμφάνιση περισσότερων",
"showLess": "Εμφάνιση λιγότερων",
"openParentFolder": "Άνοιγμα γονικού φακέλου"
"openParentFolder": "Άνοιγμα γονικού φακέλου",
"selectedCount": "Αριθμός επιλεγμένων στοιχείων για την εκτέλεση μιας ενέργειας"
},
"download": {
"downloadFile": "Λήψη αρχείου",
@@ -87,15 +88,15 @@
"click": "επιλέξτε αρχείο ή φάκελο",
"ctrl": {
"click": "επιλογή πολλαπλών αρχείων ή φακέλων",
"f": "ανοίγει την αναζήτηση",
"s": "αποθηκεύει ένα αρχείο ή εκκινεί λήψη του φακέλου στον οποίο βρίσκεστε"
"d": "λήψη επιλεγμένων στοιχείων"
},
"del": "διαγραφή επιλεγμένων στοιχείων",
"doubleClick": "ανοίγει ένα αρχείο ή φάκελο",
"esc": "καθαρίζει την επιλογή ή/και κλείνει το παράθυρο",
"f1": "αυτή η πληροφορία",
"f2": "μετονομασία αρχείου",
"help": "Βοήθεια"
"help": "Βοήθεια",
"f1": "Εμφάνιση προτροπής βοήθειας",
"description": "Μπορείτε να δείτε τις βασικές επιλογές πλοήγησης παρακάτω. Για πρόσθετες πληροφορίες, επισκεφθείτε τη διεύθυνση",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -137,16 +138,13 @@
"prompts": {
"copy": "Αντιγραφή",
"copyMessage": "Επιλέξτε τοποθεσία για αντιγραφή των αρχείων σας:",
"deleteMessageMultiple": "Είστε σίγουροι ότι θέλετε να διαγράψετε {count} αρχεία;",
"deleteMessageSingle": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το αρχείο/φάκελο;",
"deleteMessageShare": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την κοινοποίηση ({path});",
"deleteTitle": "Διαγραφή αρχείων",
"displayName": "Εμφάνιση ονόματος:",
"download": "Λήψη αρχείων",
"downloadMessage": "Επιλέξτε τη μορφή που θέλετε να λάβετε.",
"error": "Προέκυψε κάποιο σφάλμα",
"fileInfo": "Πληροφορίες αρχείου",
"lastModified": "Τελευταία τροποποίηση{suffix}",
"move": "Μετακίνηση",
"moveMessage": "Επιλέξτε νέα τοποθεσία για τα αρχεία / τους φακέλους σας:",
"newArchetype": "Δημιουργία νέας ανάρτησης με βάση έναν αρχέτυπο. Το αρχείο σας θα δημιουργηθεί στο φάκελο περιεχομένου.",
@@ -154,10 +152,7 @@
"newDirMessage": "Γράψτε το όνομα του νέου φακέλου.",
"newFile": "Νέο αρχείο",
"newFileMessage": "Γράψτε το όνομα του νέου αρχείου.",
"numberDirs": "Αριθμός φακέλων{suffix}",
"numberFiles": "Αριθμός αρχείων{suffix}",
"rename": "Μετονομασία",
"renameMessage": "Εισαγάγετε ένα νέο όνομα για το",
"replace": "Αντικατάσταση",
"replaceMessage": "Ένα από τα αρχεία που προσπαθείτε να μεταφορτώσετε δημιουργεί σύγκρουση με υπάρχον αρχείο λόγω του ονόματός του. Θέλετε να συνεχίσετε τη μεταφόρτωση ή να αντικαταστήσετε το υπάρχον;\n",
"schedule": "Προγραμματισμός",
@@ -175,10 +170,6 @@
"dragAndDrop": "Σύρετε και αφήστε αρχεία εδώ",
"completed": "Ολοκληρωμένο",
"conflictsDetected": "Ανιχνεύονται συγκρούσεις",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Μη έγκυρο όνομα: δεν μπορεί να γίνει container \"/\" ή \"\\\"",
"source": "Source{suffix}",
"downloadsLimit": "Όριο αριθμού λήψεων",
"maxBandwidth": "Μέγιστο εύρος ζώνης λήψης (kbps)",
"shareTheme": "Μοιραστείτε το θέμα",
@@ -189,22 +180,44 @@
"shareBanner": "Μοιραστείτε το URL του banner",
"shareFavicon": "Μοιραστείτε το URL favicon",
"optionalPasswordHelp": "Εάν οριστεί, οι χρήστες πρέπει να εισάγουν αυτόν τον κωδικό πρόσβασης για να προβάλουν το κοινόχρηστο περιεχόμενο.",
"viewMode": "Λειτουργία προβολής"
"viewMode": "Λειτουργία προβολής",
"renameMessage": "Εισάγετε ένα νέο όνομα:",
"renameMessageInvalid": "Ένα αρχείο δεν μπορεί να περιέχει \"/\" ή \"\\\"",
"source": "Πηγή{suffix}",
"deleteMessageMultiple": "Είστε σίγουροι ότι θέλετε να διαγράψετε το/τα αρχείο/α {count};",
"deleteMessageShare": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το μερίδιο: {path} ?",
"lastModified": "Τελευταία τροποποίηση{suffix}",
"numberDirs": "Αριθμός καταλόγων{suffix}",
"numberFiles": "Αριθμός αρχείων{suffix}",
"renameMessageConflict": "Ένα στοιχείο με όνομα \"{filename}\" υπάρχει ήδη!",
"uploadSettingsChunked": "Μέγιστες ταυτόχρονες μεταφορτώσεις: {maxConcurrentUpload}, μέγεθος τεμαχίου: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Μέγιστες ταυτόχρονες μεταφορτώσεις: {maxConcurrentUpload}, chunked upload disabled"
},
"search": {
"images": "Εικόνες",
"music": "Μουσική",
"pdf": "PDF",
"pressToSearch": "Πατήστε Enter για αναζήτηση…",
"search": "Αναζήτηση…",
"typeToSearch": "Πληκτρολογήστε για αναζήτηση…",
"types": "Τύποι",
"video": "Βίντεο",
"path": "Μονοπάτι:",
"smallerThan": "Μικρότερο από:",
"largerThan": "Μεγαλύτερο από:",
"helpText1": "Η αναζήτηση πραγματοποιείται σε κάθε χαρακτήρα που πληκτρολογείτε (ελάχιστο όριο 3 χαρακτήρων για τους όρους αναζήτησης).",
"helpText2": "Ο δείκτης: Η αναζήτηση χρησιμοποιεί το ευρετήριο το οποίο ενημερώνεται αυτόματα στο ρυθμισμένο διάστημα (προεπιλογή: 5 λεπτά). Η αναζήτηση όταν το πρόγραμμα έχει μόλις ξεκινήσει μπορεί να οδηγήσει σε ελλιπή αποτελέσματα."
"helpText2": "Ο δείκτης: Η αναζήτηση χρησιμοποιεί το ευρετήριο το οποίο ενημερώνεται αυτόματα στο ρυθμισμένο διάστημα (προεπιλογή: 5 λεπτά). Η αναζήτηση όταν το πρόγραμμα έχει μόλις ξεκινήσει μπορεί να οδηγήσει σε ελλιπή αποτελέσματα.",
"noResults": "Δεν βρέθηκαν αποτελέσματα στην ευρετηριασμένη αναζήτηση.",
"onlyFolders": "Μόνο φάκελοι",
"onlyFiles": "Μόνο αρχεία",
"showOptions": "Εμφάνιση επιλογών",
"photos": "Φωτογραφίες",
"audio": "Ήχος",
"videos": "Βίντεο",
"documents": "Έγγραφα",
"archives": "Αρχεία",
"number": "Αριθμός",
"searchContext": "Πλαίσιο αναζήτησης: {context}",
"notEnoughCharacters": "Δεν υπάρχουν αρκετοί χαρακτήρες για αναζήτηση (min {minSearchLength})",
"typeToSearch": "Αρχίστε να πληκτρολογείτε {minSearchLength} ή περισσότερους χαρακτήρες για να ξεκινήσετε την αναζήτηση."
},
"settings": {
"admin": "Διαχειριστής",
@@ -217,10 +230,8 @@
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
"branding": "Εξατομίκευση",
"brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης",
"brandingHelp": "Μπορείτε να προσαρμόσετε την εμφάνισης της εφαρμογής File Browser αλλάζοντας το όνομά της, αντικαθιστώντας το λογότυπό της, προσθέτοντας προσαρμοσμένα στυλ και ακόμα και απενεργοποιώντας εξωτερικούς συνδέσμους προς το GitHub.\nΓια περισσότερες πληροφορίες σχετικά με αυτές τις προσαρμογές, ελέγξτε το {0}.",
"changePassword": "Αλλαγή κωδικού πρόσβασης",
"commandRunner": "Εκτέλεση εντολών",
"commandRunnerHelp": "Εδώ μπορείτε να ορίσετε εντολές που εκτελούνται στα ονομασμένα γεγονότα και δραστηριότητες. Πρέπει να γράψετε μία εντολή ανά γραμμή. Οι μεταβλητές περιβάλλοντος {0} και {1} θα είναι διαθέσιμες, και θα είναι {0} σχετικές με το {1}. Για περισσότερες πληροφορίες σχετικά με αυτή τη λειτουργία και τις διαθέσιμες μεταβλητές περιβάλλοντος, παρακαλώ διαβάστε το {2}.",
"commandsUpdated": "Οι εντολές ενημερώθηκαν!",
"createUserDir": "Αυτόματη δημιουργία φακέλου χρήστη κατά την προσθήκη νέου χρήστη",
"tusUploads": "Τμηματικές μεταφορές αρχείων",
@@ -303,7 +314,14 @@
"adminOptions": "Επιλογές χρήστη διαχειριστή",
"shareDuration": "Λήγει",
"searchOptions": "Επιλογές αναζήτησης",
"downloads": "Λήψεις"
"downloads": "Λήψεις",
"systemAdmin": "Σύστημα & Διαχείριση",
"configViewer": "Config Viewer",
"configViewerShowFull": "Εμφάνιση πλήρων ρυθμίσεων",
"configViewerShowComments": "Εμφάνιση σχολίων",
"configViewerLoadConfig": "Φόρτωση Config",
"brandingHelp": "Μπορείτε να προσαρμόσετε την εμφάνιση και την αίσθηση του File Browser αλλάζοντας το όνομά του, αντικαθιστώντας το λογότυπο, προσθέτοντας προσαρμοσμένα στυλ και ακόμη και απενεργοποιώντας τους εξωτερικούς συνδέσμους προς το GitHub.\nΓια περισσότερες πληροφορίες σχετικά με την προσαρμοσμένη επωνυμία, ανατρέξτε στη διεύθυνση {0}.",
"commandRunnerHelp": "Εδώ μπορείτε να ορίσετε τις εντολές που εκτελούνται στα συμβάντα που ονομάζονται. Πρέπει να γράψετε μία ανά γραμμή. Οι περιβαλλοντικές μεταβλητές {0} και {1} θα είναι διαθέσιμες, ενώ η {0} θα είναι σχετική με την {1}. Για περισσότερες πληροφορίες σχετικά με αυτή τη λειτουργία και τις διαθέσιμες μεταβλητές περιβάλλοντος, διαβάστε το {2}."
},
"sidebar": {
"help": "Βοήθεια",
@@ -358,12 +376,21 @@
"hideNavButtons": "Απόκρυψη κουμπιών πλοήγησης",
"hideNavButtonsDescription": "Κρύψτε τα κουμπιά πλοήγησης στη γραμμή πλοήγησης στο μερίδιο για να δημιουργήσετε μια μινιμαλιστική εμφάνιση.",
"viewModeDescription": "Προεπιλεγμένη διάταξη για την κοινή σελίδα.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Σας έχει αποσταλεί ένα share για να το δείτε ή να το κατεβάσετε.",
"disableShareCard": "Απενεργοποίηση κάρτας κοινής χρήσης",
"disableShareCardDescription": "Απενεργοποιήστε την κάρτα κοινής χρήσης στην κοινόχρηστη σελίδα στην πλαϊνή μπάρα ή στην κορυφή της σελίδας στο κινητό.",
"disableSidebar": "Απενεργοποίηση πλευρικής μπάρας",
"disableSidebarDescription": "Απενεργοποιήστε την πλαϊνή μπάρα στην κοινή σελίδα."
"disableSidebarDescription": "Απενεργοποιήστε την πλαϊνή μπάρα στην κοινή σελίδα.",
"enforceDarkLightMode": "Επιβολή λειτουργίας θέματος",
"enforceDarkLightModeDescription": "Επιβάλλει μια συγκεκριμένη λειτουργία θέματος (σκούρο ή ανοιχτό) για αυτό το κοινόχρηστο, παρακάμπτοντας τις προτιμήσεις του χρήστη.",
"default": "Μην επιβάλλετε μια λειτουργία",
"dark": "Σκούρο",
"light": "Φως",
"enableOnlyOffice": "Ενεργοποίηση της προβολής OnlyOffice",
"enableOnlyOfficeDescription": "Επιτρέψτε την προβολή αρχείων γραφείου χρησιμοποιώντας το OnlyOffice σε αυτό το κοινόχρηστο χώρο.",
"enableOnlyOfficeEditing": "Ενεργοποίηση της επεξεργασίας OnlyOffice",
"enableOnlyOfficeEditingDescription": "Επιτρέψτε την επεξεργασία αρχείων γραφείου χρησιμοποιώντας το OnlyOffice σε αυτό το κοινόχρηστο χώρο.",
"titleDefault": "Κοινόχρηστα αρχεία - {title}"
},
"api": {
"title": "Κλειδιά API:",
@@ -447,8 +474,8 @@
"totalDenied": "Απορρίπτεται",
"totalAllowed": "Επιτρεπόμενο",
"all": "Άρνηση όλων",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Αυτό δείχνει τι συμβαίνει όταν ένας χρήστης προσπαθεί να αποκτήσει πρόσβαση σε μια διαδρομή όπου δεν υπάρχουν συγκεκριμένοι κανόνες πρόσβασης. 'Allow' σημαίνει ότι οι χρήστες μπορούν να έχουν πρόσβαση από προεπιλογή, 'Deny' σημαίνει ότι οι χρήστες αποκλείονται από προεπιλογή, εκτός αν επιτρέπεται ρητά."
"defaultBehaviorDescription": "Αυτό δείχνει τι συμβαίνει όταν ένας χρήστης προσπαθεί να αποκτήσει πρόσβαση σε μια διαδρομή όπου δεν υπάρχουν συγκεκριμένοι κανόνες πρόσβασης. 'Allow' σημαίνει ότι οι χρήστες μπορούν να έχουν πρόσβαση από προεπιλογή, 'Deny' σημαίνει ότι οι χρήστες αποκλείονται από προεπιλογή, εκτός αν επιτρέπεται ρητά.",
"defaultBehavior": "Προεπιλεγμένη ενέργεια{suffix}"
},
"fileLoading": {
"title": "Ανεβάσματα & λήψεις",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - Το προεπιλεγμένο θέμα",
"customTheme": "Επιλέξτε το θέμα σας",
"showSelectMultiple": "Εμφάνιση πολλαπλών επιλογών στο μενού περιβάλλοντος στην επιφάνεια εργασίας",
"showSelectMultipleDescription": "Από προεπιλογή, το μενού περιβάλλοντος της επιφάνειας εργασίας δεν διαθέτει την επιλογή \"επιλογή πολλαπλών\" όπως το κινητό. Αυτή η επιλογή εμφανίζεται πάντα στο μενού περιβάλλοντος."
"showSelectMultipleDescription": "Από προεπιλογή, το μενού περιβάλλοντος της επιφάνειας εργασίας δεν διαθέτει την επιλογή \"επιλογή πολλαπλών\" όπως το κινητό. Αυτή η επιλογή εμφανίζεται πάντα στο μενού περιβάλλοντος.",
"defaultMediaPlayer": "Χρησιμοποιήστε το εγγενές πρόγραμμα αναπαραγωγής πολυμέσων του προγράμματος περιήγησής σας",
"defaultMediaPlayerDescription": "Χρησιμοποιήστε το εγγενές πρόγραμμα αναπαραγωγής πολυμέσων του προγράμματος περιήγησης για αρχεία βίντεο και ήχου αντί για το πρόγραμμα αναπαραγωγής πολυμέσων \"vue-plyr\" που περιλαμβάνεται στο FileBrowser.",
"debugOfficeEditor": "Ενεργοποίηση λειτουργίας εντοπισμού σφαλμάτων OnlyOffice",
"debugOfficeEditorDescription": "Εμφάνιση ενός αναδυόμενου παραθύρου εντοπισμού σφαλμάτων με πρόσθετες πληροφορίες στον επεξεργαστή OnlyOffice.",
"fileViewerOptions": "Επιλογές προβολής αρχείων",
"themeAndLanguage": "Θέμα & Γλώσσα"
},
"editor": {
"uninitialized": "Απέτυχε η αρχικοποίηση του συντάκτη. Παρακαλούμε επαναφορτώστε.",
"saveFailed": "Απέτυχε η αποθήκευση του αρχείου. Προσπαθήστε ξανά.",
"saveAborted": "Η αποθήκευση διακόπηκε. Το όνομα του αρχείου δεν ταιριάζει με το ενεργό αρχείο.",
"saveDisabled": "Δεν υποστηρίζεται η αποθήκευση κοινόχρηστων αρχείων.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Διακοπή της λειτουργίας αποθήκευσης. Το ενεργό αρχείο της εφαρμογής ({activeFile}) δεν ταιριάζει με το αρχείο που προσπαθείτε να αποθηκεύσετε ({tryingToSave}).",
"syncFailed": "απέτυχε να συγχρονίσει την κατάσταση με τη διαδρομή για το \"{filename}\" μετά από 5 προσπάθειες. Διακοπή της ρύθμισης του συντάκτη για να αποφευχθεί η αλλοίωση των δεδομένων."
},
"player": {
"LoopEnabled": "Ενεργοποίηση βρόχου",
"LoopDisabled": "Βρόχος απενεργοποιημένος"
}
}
+50 -13
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Clear completed",
"showMore": "Show more",
"showLess": "Show less",
"openParentFolder": "Open parent folder"
"openParentFolder": "Open parent folder",
"selectedCount": "Number of items selected to perform an action"
},
"download": {
"downloadFile": "Download File",
@@ -87,15 +88,15 @@
"click": "select file or directory",
"ctrl": {
"click": "select multiple files or directories",
"f": "opens search",
"s": "save a file or download the directory where you are"
"d": "download selected items"
},
"del": "delete selected items",
"doubleClick": "open a file or directory",
"esc": "clear selection and/or close the prompt",
"f1": "this information",
"f2": "rename file",
"help": "Help"
"f1": "show help prompt",
"help": "Help",
"description": "You can view the basic naviagation options below. For additional information, please visit",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -166,7 +167,16 @@
"disableShareCard": "Disable share card",
"disableShareCardDescription": "Disable the share card on the shared page in the sidebar or at the top of the page on mobile.",
"disableSidebar": "Disable sidebar",
"disableSidebarDescription": "Disable the sidebar on the shared page."
"disableSidebarDescription": "Disable the sidebar on the shared page.",
"enforceDarkLightMode": "Enforce theme mode",
"enforceDarkLightModeDescription": "Force a specific theme mode (dark or light) for this share, overriding user preferences.",
"default": "Do not enforce a mode",
"dark": "Dark",
"light": "Light",
"enableOnlyOffice": "Enable OnlyOffice viewer",
"enableOnlyOfficeDescription": "Allow viewing office files using OnlyOffice in this share.",
"enableOnlyOfficeEditing": "Enable OnlyOffice editing",
"enableOnlyOfficeEditingDescription": "Allow editing of office files using OnlyOffice in this share."
},
"prompts": {
"downloadsLimit": "Number of downloads limit",
@@ -180,7 +190,6 @@
"shareBanner": "Share banner URL",
"shareFavicon": "Share favicon URL",
"source": "Source{suffix}",
"invalidName": "Invalid name provided: cannot container \"/\" or \"\\\"",
"destinationSource": "Destination Source:",
"copy": "Copy",
"copyMessage": "Choose the place to copy your files:",
@@ -209,7 +218,9 @@
"numberDirs": "Number of directories{suffix}",
"numberFiles": "Number of files{suffix}",
"rename": "Rename",
"renameMessage": "An item named \"{filename}\" already exists! Enter a new name:",
"renameMessageConflict": "An item named \"{filename}\" already exists!",
"renameMessage": "Enter a new name:",
"renameMessageInvalid": "A file can't container \"/\" or \"\\\"",
"replace": "Replace",
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to continue to upload or replace the existing one?\n",
"schedule": "Schedule",
@@ -226,12 +237,23 @@
"conflictsDetected": "Conflicts detected"
},
"search": {
"searchContext": "Search Context: {context}",
"onlyFolders": "Only Folders",
"onlyFiles": "Only Files",
"showOptions": "Show Options",
"photos": "Photos",
"audio": "Audio",
"videos": "Videos",
"documents": "Documents",
"archives": "Archives",
"notEnoughCharacters": "Not enough characters to search (min {minSearchLength})",
"typeToSearch": "Start typing {minSearchLength} or more characters to begin searching.",
"images": "Images",
"music": "Music",
"pdf": "PDF",
"pressToSearch": "No results found in indexed search.",
"search": "Search...",
"typeToSearch": "Type to search... (3 character minimum)",
"number": "Number",
"noResults": "No results found in indexed search.",
"path": "Path:",
"types": "Types",
"video": "Video",
@@ -241,6 +263,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": {
"debugOfficeEditor": "Enable OnlyOffice Debug Mode",
"debugOfficeEditorDescription": "Show a debug popup with additional information in the OnlyOffice editor.",
"defaultMediaPlayer": "Use your browser's native media player",
"defaultMediaPlayerDescription": "Use your browser's native media player for video and audio files rather than FileBrowser's included 'vue-plyr' media player.",
"showSelectMultiple": "Show select multiple in context menu on desktop",
"showSelectMultipleDescription": "By default, desktop context menu does not have \"select multiple\" option like mobile. This option always shows it in the context menu.",
"customTheme": "Choose your theme",
@@ -289,9 +315,16 @@
"disablePreviewExt": "Disable office file previews for certain file extensions",
"disablePreviewExtDescription": "A space-separated list of file extensions to disable office file previews for (e.g., '.docx .pptx').",
"disableOfficeEditorDescription": "A space-separated list of file extensions to disable the OnlyOffice editor and viewer for. (e.g., '.txt .html .pdf').",
"disableOfficeEditor": "Disable office viewer for certain file extensions"
"disableOfficeEditor": "Disable office viewer for certain file extensions",
"fileViewerOptions": "File Viewer Options",
"themeAndLanguage": "Theme & Language"
},
"settings": {
"systemAdmin": "System & Admin",
"configViewer": "Config Viewer",
"configViewerShowFull": "Show full config",
"configViewerShowComments": "Show comments",
"configViewerLoadConfig": "Load Config",
"enterPassword": "Enter password",
"enterPasswordAgain": "Enter the password again",
"passwordsDoNotMatch": "Passwords do not match",
@@ -523,5 +556,9 @@
"saveDisabled":"Saving shared files is not supported.",
"saveAbortedMessage":"Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed":"failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
},
"player": {
"LoopEnabled": "Loop Enabled",
"LoopDisabled": "Loop Disabled"
}
}
}
+133 -96
View File
@@ -31,25 +31,26 @@
"share": "Compartir",
"shell": "Presiona Enter para buscar...",
"submit": "Enviar",
"switchView": "Cambiar vista",
"toggleSidebar": "Alternar menú",
"switchView": "Cambiar modo de visualización",
"toggleSidebar": "Cambiar visibilidad del panel lateral",
"update": "Actualizar",
"upload": "Subir",
"openFile": "Abrir archivo",
"copyDownloadLinkToClipboard": "Copiar enlace de descarga",
"continue": "Continuar",
"edit": "Editar",
"generateNewOtp": "Generar nuevo código 2FA",
"generateNewOtp": "Generar nuevo código de dos factores (2FA)",
"verify": "Verificar",
"pause": "Pausar",
"resume": "Reanudar",
"retry": "Reintentar",
"pauseAll": "Pausar todo",
"resumeAll": "Reanudar todo",
"clearCompleted": "Limpiar lista",
"clearCompleted": "Limpiar completados",
"showMore": "Ver más",
"showLess": "Mostrar menos",
"openParentFolder": "Abrir carpeta principal"
"openParentFolder": "Abrir carpeta principal",
"selectedCount": "Número de elementos seleccionados para realizar una acción"
},
"download": {
"downloadFile": "Descargar archivo",
@@ -61,7 +62,7 @@
"internal": "Error interno :(",
"notFound": "No se encontró el directorio",
"connection": "No se puede conectar al servidor.",
"shareNotFound": "Acción no válida o caducada."
"shareNotFound": "No se encuentra lo que buscas, tal vez el enlace expiró."
},
"files": {
"body": "Contenido",
@@ -75,7 +76,7 @@
"multipleSelectionEnabled": "Selección múltiple activada",
"name": "Nombre",
"size": "Tamaño",
"sortByLastModified": "Ordenar por modificación",
"sortByLastModified": "Ordenar por fecha de modificación",
"sortByName": "Ordenar por nombre",
"sortBySize": "Ordenar por tamaño",
"noPreview": "Vista previa no disponible"
@@ -84,15 +85,15 @@
"click": "Selecciona un archivo o carpeta",
"ctrl": {
"click": "Selecciona varios archivos o carpetas (Selección multiple)",
"f": "Abrir panel de búsqueda",
"s": "Guardar archivo (solo funciona si estás en el editor)"
"d": "descargar artículos seleccionados"
},
"del": "Eliminar archivos/carpetas seleccionados",
"doubleClick": "Abre un archivo o carpeta",
"esc": "Cancela la selección o cierra la ventana actual (ej: Para salir rápido cuando estas viendo una imagen o video)",
"f1": "Esta ventana",
"f2": "Cambiar nombre del archivo",
"help": "Ayuda"
"f2": "Cambiar nombre del archivo o carpeta",
"help": "Ayuda",
"f1": "mostrar mensaje de ayuda",
"description": "A continuación puede ver las opciones básicas de navegación. Para más información, visite",
"wiki": "Wiki de FileBrowser Quantum"
},
"languages": {
"he": "עברית",
@@ -134,16 +135,13 @@
"prompts": {
"copy": "Copiar",
"copyMessage": "Selecciona una ubicación para copiar",
"deleteMessageMultiple": "¿Estás seguro que quieres eliminar {count} archivo(s)?",
"deleteMessageSingle": "¿Estás seguro que quieres eliminar este archivo/carpeta?",
"deleteMessageShare": "¿Estás seguro que quieres eliminar este recurso compartido ({path})?",
"deleteTitle": "Borrar archivos",
"displayName": "Nombre:",
"download": "Descargar archivos",
"downloadMessage": "Elige el formato de descarga",
"error": "Ha ocurrido un error",
"fileInfo": "Información del archivo",
"lastModified": "Última modificación{suffix}",
"move": "Mover",
"moveMessage": "Selecciona un nuevo destino:",
"newArchetype": "Crear una nueva publicación en base a una plantilla, tu archivo se creara en la misma carpeta",
@@ -151,10 +149,7 @@
"newDirMessage": "Nombre de la carpeta",
"newFile": "Nuevo archivo",
"newFileMessage": "Nombre del archivo",
"numberDirs": "Carpetas",
"numberFiles": "Archivos",
"rename": "Cambiar nombre",
"renameMessage": "Nuevo nombre para:",
"replace": "Reemplazar",
"replaceMessage": "Un archivo con el mismo nombre ya existe. ¿Quieres cambiar el nombre?\n",
"schedule": "Programar",
@@ -169,39 +164,57 @@
"selected": "seleccionado",
"deleted": "¡Borrado con éxito!",
"destinationSource": "Destino:",
"source": "Source{suffix}",
"downloadsLimit": "Límite de descargas",
"maxBandwidth": "Ancho de banda máximo de descarga (kbps)",
"shareTheme": "Compartir tema",
"shareThemeColor": "Compartir color del tema (valor CSS)",
"shareTitle": "Compartir título",
"shareDescription": "Compartir descripción",
"shareLogo": "Compartir URL del logotipo",
"shareBanner": "Compartir URL del banner",
"shareFavicon": "Compartir URL de favicon",
"optionalPasswordHelp": "Si se establece, los usuarios deben introducir esta contraseña para ver el contenido compartido.",
"maxBandwidth": "Velocidad máxima de descarga (kbps)",
"shareTheme": "Tema",
"shareThemeColor": "Color del tema (valor CSS)",
"shareTitle": "Título la página",
"shareDescription": "Descripción de la página",
"shareLogo": "Imagen/Logo de la página",
"shareBanner": "Banner de la página",
"shareFavicon": "Mini-icono de la página (favicon)",
"optionalPasswordHelp": "Si eliges una contraseña, los usuarios que accedan deben introducir esa misma contraseña para ver el contenido.",
"dragAndDrop": "Arrastra y suelta los archivos aquí",
"completed": "Completado",
"conflictsDetected": "Conflictos detectados",
"uploadSettingsChunked": "Subidas simultáneas: {maxConcurrentUpload}, velocidad de subida máxima: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Subidas simultáneas: {maxConcurrentUpload}, subida de archivos desactivada",
"invalidName": "Nombre inválido: no puede contener \"/\" o \"\\\".",
"viewMode": "Ver modo"
"viewMode": "Cambiar modo de visualización",
"renameMessage": "Introduce un nuevo nombre:",
"renameMessageInvalid": "Un archivo no puede contener \"/\" o \"\\\"",
"source": "Fuente{suffix}",
"deleteMessageMultiple": "¿Está seguro de que desea eliminar los archivos de {count}?",
"deleteMessageShare": "¿Estás seguro de que quieres borrar esta acción? {path} ?",
"lastModified": "Última modificación{suffix}",
"numberDirs": "Número de directorios{suffix}",
"numberFiles": "Número de ficheros{suffix}",
"renameMessageConflict": "Ya existe un elemento llamado \"{filename}\".",
"uploadSettingsChunked": "Carga máxima simultánea: {maxConcurrentUpload} Tamaño del chunk {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Carga máxima simultánea: {maxConcurrentUpload} Carga por trozos desactivada"
},
"search": {
"images": "Imágenes",
"music": "Música",
"pdf": "PDF",
"pressToSearch": "Enter para buscar...",
"search": "Buscar...",
"typeToSearch": "Escribe para buscar...",
"types": "Tipos",
"video": "Vídeo",
"path": "Ruta:",
"smallerThan": "Menor que:",
"largerThan": "Mayor que:",
"helpText1": "La búsqueda se realiza por cada carácter (mínimo de 3 caracteres)",
"helpText2": "El índice se actualiza automáticamente (cada 5 min por defecto). Si es interrumpido pueden ocurrir errores"
"helpText2": "El índice se actualiza automáticamente (cada 5 min por defecto). Si es interrumpido pueden ocurrir errores",
"onlyFolders": "Sólo carpetas",
"onlyFiles": "Sólo archivos",
"showOptions": "Mostrar opciones",
"photos": "Fotos",
"audio": "Audio",
"videos": "Vídeos",
"documents": "Documentos",
"archives": "Archivos",
"number": "Número",
"noResults": "No se han encontrado resultados en la búsqueda indexada.",
"searchContext": "Contexto de búsqueda: {context}",
"notEnoughCharacters": "No hay suficientes caracteres para buscar (min {minSearchLength})",
"typeToSearch": "Escriba {minSearchLength} o más caracteres para iniciar la búsqueda."
},
"settings": {
"scopes": "Establece los directorios y los permisos que el usuario puede acceder",
@@ -215,10 +228,8 @@
"avoidChanges": "(dejar en blanco para evitar cambios)",
"branding": "Personalización",
"brandingDirectoryPath": "Directorio de tus archivos que usarás para personalizar FileBrowser",
"brandingHelp": "Personaliza FileBrowser cambiando el nombre, logo, estilos. Más información en {0}",
"changePassword": "Cambiar contraseña",
"commandRunner": "Ejecutor de comandos",
"commandRunnerHelp": "Comandos para eventos. Variables disponibles: {0} (ruta relativa), {1} (ruta base). Más info en {2}",
"commandsUpdated": "¡Comandos actualizados!",
"createUserDir": "Crea automaticamente una carpeta cuando se agrega un nuevo usuario",
"userHomeBasePath": "Ruta base para los directorios personales de los usuarios",
@@ -249,7 +260,7 @@
"modify": "Editar archivos",
"share": "Compartir archivos",
"admin": "Administrador",
"api": "Gestionar claves API",
"api": "Gestión de APIs",
"realtime": "Actualizaciones en tiempo real"
},
"permissions-name": "Permisos",
@@ -261,7 +272,7 @@
"rulesHelp": "Puedes definir un conjunto de reglas de permisos para este usuario específico. Los archivos bloqueados no se mostrarán en las listas y no serán accesibles por el usuario. Puedes utilizar regex y rutas relativas a la raíz del usuario.\n",
"scope": "Raíz",
"settingsUpdated": "¡Ajustes actualizados!",
"shareManagement": "Gestión Compartida",
"shareManagement": "Gestión de recursos compartidos",
"shareDeleted": "¡Compartido eliminado!",
"singleClick": "Abrir con un solo clic",
"themes": {
@@ -275,7 +286,7 @@
"userCreated": "¡Usuario creado!",
"userDefaults": "Configuración de usuario por defecto",
"userDeleted": "¡Usuario eliminado!",
"userManagement": "Gestionar usuarios",
"userManagement": "Gestión de usuarios",
"userUpdated": "¡Usuario actualizado!",
"username": "Nombre de Usuario",
"users": "Usuarios",
@@ -287,20 +298,27 @@
"UserManagement": "Gestión de usuarios",
"tusUploads": "Subir en partes",
"tusUploadsHelp": "File Browser Quantum permite subir archivos en partes, lo que permite que sean subidos de manera eficiente, fiable, que sea reanudable, incluso si tu internet es inestable",
"tusUploadsChunkSize": "Tamaño máximo de subida de un archivo. Puede introducir un número entero que indique una entrada en bytes o una cadena como 10 MB, 1 GB, etc",
"tusUploadsChunkSize": "Tamaño máximo de subida de un archivo. Puede escribir un número entero que indique una entrada en bytes o una cadena como 10 MB, 1 GB, etc",
"tusUploadsRetryCount": "Número de reintentos, si falla la subida de un archivo",
"api": "Claves API",
"loginMethodDescription": "Método de inicio de sesión permitido",
"userNotAdmin": "El usuario no es administrador",
"enterPassword": "Indusca la contraseña",
"enterPasswordAgain": "Vuelva a introducir la contraseña",
"enterPassword": "Escriba su contraseña",
"enterPasswordAgain": "Vuelva a escribir su contraseña",
"passwordsDoNotMatch": "Las contraseñas no coinciden",
"loginMethod": "Método de inicio de sesión",
"shareSettings": "Compartidos",
"shareSettings": "Recursos compartidos",
"adminOptions": "Opciones de administrador",
"shareDuration": "Expira en",
"searchOptions": "Opciones de búsqueda",
"downloads": "Descargas"
"downloads": "Descargas",
"systemAdmin": "Sistema y administración",
"configViewer": "Visor de configuración",
"configViewerShowFull": "Mostrar configuración completa",
"configViewerShowComments": "Mostrar comentarios",
"configViewerLoadConfig": "Configuración de carga",
"brandingHelp": "Puede personalizar el aspecto de su instancia del Navegador de archivos cambiando su nombre, sustituyendo el logotipo, añadiendo estilos personalizados e incluso desactivando los enlaces externos a GitHub.\nPara obtener más información sobre la personalización de la marca, consulta la página {0}.",
"commandRunnerHelp": "Aquí puede establecer los comandos que se ejecutan en los eventos nombrados. Debe escribir uno por línea. Las variables de entorno {0} y {1} estarán disponibles, siendo {0} relativa a {1}. Para más información sobre esta característica y las variables de entorno disponibles, por favor lea el {2}."
},
"sidebar": {
"help": "Ayuda",
@@ -333,61 +351,70 @@
},
"share": {
"notice": "Nota: cuando se crea un recurso compartido, queda más expuesto al público y tiene diferentes normas de acceso. Si su recurso compartido no está configurado de forma segura, cualquiera puede acceder a él.",
"disableAnonymousDescription": "Cuando está activada, sólo los usuarios autenticados pueden acceder al recurso compartido. El usuario debe tener acceso al origen del recurso compartido.",
"disableAnonymousDescription": "Cuando está activado, sólo los usuarios con su permiso pueden acceder al recurso compartido. El usuario debe tener acceso al origen recurso compartido.",
"disableThumbnailsDescription": "Si está activada, las miniaturas de previsualización no se mostrarán en la acción.",
"enableAllowedUsernamesDescription": "Cuando se activa, sólo los usuarios especificados pueden acceder al recurso compartido. El usuario debe tener acceso al origen del recurso compartido.",
"enableAllowedUsernamesDescription": "Cuando se activa, sólo los usuarios que especifique pueden acceder al recurso compartido. El usuario debe tener acceso al origen del recurso compartido.",
"downloadsLimitDescription": "Número máximo de veces que se puede descargar un archivo/carpeta del recurso compartido. Dejar vacío para ilimitado.",
"maxBandwidthDescription": "El ancho de banda máximo de descarga en kilobytes por segundo. Dejar vacío para ilimitado.",
"shareThemeDescription": "El tema que se utilizará para el enlace compartido.",
"disableAnonymous": "Desactivar el acceso anónimo",
"disableThumbnails": "Desactivar miniaturas",
"enableAllowedUsernames": "Compartir sólo con determinados usuarios",
"allowedUsernamesPlaceholder": "Introduzca los nombres de usuario, separados por comas",
"keepAfterExpiration": "No eliminar la acción una vez caducada",
"keepAfterExpirationDescription": "La acción no se borrará después de que caduque. Esto resulta útil si desea ampliar el plazo de expiración de una acción o editarla después de que expire.",
"allowedUsernamesPlaceholder": "Escriba los nombres de usuario (separado por comas)",
"keepAfterExpiration": "No eliminar el recurso compartido una vez expirado",
"keepAfterExpirationDescription": "Los recursos compartidos no se borraran cuando expiren. Es útil si después quieres ampliar la duración del recurso, o editarlo.",
"saveDisabled": "No se pueden guardar los cambios en los archivos compartidos.",
"shareDurationDescription": "Duración antes de que caduque la acción. Dejar en blanco para una acción permanente.",
"passwordDescription": "Contraseña opcional necesaria para acceder a este recurso compartido, incluidos los usuarios autenticados.",
"shareDurationDescription": "Duración del recurso compartido. Dejar en blanco para permanente.",
"passwordDescription": "Contraseña opcional necesaria para acceder al recurso compartido, incluidos los usuarios autenticados.",
"allowedUsernamesListDescription": "Nombres de usuario separados por comas que pueden acceder al recurso compartido (si está activado).",
"shareThemeColorDescription": "Valor de color CSS aplicado al tema de la acción (por ejemplo, rojo, #0ea5e9, rgb(14,165,233)).",
"shareTitleDescription": "Título de página personalizado que se muestra en la página de compartición.",
"shareDescriptionHelp": "Breve descripción mostrada en la página de compartición (puede utilizarse en las metaetiquetas).",
"shareLogoDescription": "URL o ruta de la imagen del logotipo accesible por el cliente -- se aceptan rutas abosolutas o de índice: por ejemplo, 'https://domain.com/logo.png' o '/ruta/a/logo.png'.",
"shareBannerDescription": "URL de la imagen del banner o ruta accesible por el cliente -- se aceptan rutas abosolutas o de índice: por ejemplo, 'https://domain.com/banner.png' o '/ruta/a/banner.png'.",
"shareFaviconDescription": "URL del favicon o ruta accesible por el cliente -- se aceptan rutas abosolutas o de índice: por ejemplo 'https://domain.com/favicon.png' o '/ruta/a/favicon.png'.",
"shareTitleDescription": "Título de página personalizado de página",
"shareDescriptionHelp": "Breve descripción mostrada en la página (que suele aparecer cuando compartes el enlace)",
"shareLogoDescription": "Enlace (URL), o ruta de la imagen del logo que se mostrará en el enlace -- por ejemplo, 'https://ejemplo.com/logo.png' o '/ruta/a/logo.png'.",
"shareBannerDescription": "Enlace (URL), o ruta de la imagen del banner que se mostrará en el enlace -- por ejemplo, 'https://ejemplo.com/banner.png' o '/ruta/a/banner.png'.",
"shareFaviconDescription": "Enlace (URL) del icono (favicon) que se mostrará en el enlace -- por ejemplo 'https://domain.com/favicon.png' o '/ruta/a/favicon.png'",
"hideNavButtons": "Ocultar botones de navegación",
"hideNavButtonsDescription": "Oculte los botones de navegación de la barra de navegación en la acción para crear un aspecto minimalista.",
"viewModeDescription": "Diseño por defecto de la página compartida.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Se le ha enviado una acción para que la vea o descargue.",
"disableShareCard": "Desactivar tarjeta compartida",
"disableShareCardDescription": "Desactivar la tarjeta de compartir en la página compartida en la barra lateral o en la parte superior de la página en móvil.",
"disableSidebar": "Desactivar la barra lateral",
"disableSidebarDescription": "Desactivar la barra lateral en la página compartida."
"disableShareCardDescription": "Desactivar la tarjeta de compartir en la página compartida en el panel lateral o en la parte superior de la página en móvil.",
"disableSidebar": "Desactivar el panel lateral",
"disableSidebarDescription": "Desactivar el panel lateral en la página compartida.",
"enforceDarkLightMode": "Aplicar el modo temático",
"enforceDarkLightModeDescription": "Forzar un modo de tema específico (oscuro o claro) para esta acción, anulando las preferencias del usuario.",
"default": "No imponer un modo",
"dark": "Oscuro",
"light": "Luz",
"enableOnlyOffice": "Activar el visor de OnlyOffice",
"enableOnlyOfficeDescription": "Permitir la visualización de archivos de oficina utilizando OnlyOffice en este recurso compartido.",
"enableOnlyOfficeEditing": "Habilitar la edición de OnlyOffice",
"enableOnlyOfficeEditingDescription": "Permitir la edición de archivos de oficina utilizando OnlyOffice en este recurso compartido.",
"titleDefault": "Archivos compartidos - {title}"
},
"api": {
"title": "Crear API:",
"keyName": "Nombre de la clave:",
"title": "Claves API",
"keyName": "Nombre para la API",
"createdAt": "Creado en:",
"expiresAt": "Expira en:",
"permissions": "Permisos:",
"createTitle": "Crear clave API",
"keyNamePlaceholder": "Nombre único para clave API:",
"keyNamePlaceholder": "Escriba un nombre único",
"tokenDuration": "Duración:",
"durationNumberPlaceholder": "Número:",
"days": "Días",
"months": "Meses",
"swaggerLinkText": "Documentación de Swagger",
"swaggerLinkText": "Documentación de Swagger (API)",
"name": "Nombre",
"created": "Creado",
"expires": "Expira en",
"expires": "Expira en:",
"actions": "Acciones",
"enabled": "Habilitado",
"disabled": "Deshabilitado",
"permissionNote": "Elija al menos un permiso para la clave. Su usuario también debe tener el permiso.",
"permissionNote": "Elija al menos un permiso para la API. Tu usuario también debe tener ese permiso.",
"createKeyFailed": "Error al crear la clave API.",
"createKeySuccess": "Clave API creada correctamente.",
"description": "Las claves API se basan en el usuario que las crea. Véase"
"createKeySuccess": "¡Clave API creada con exito!",
"description": "Las claves API solo podrán tener los permisos que el usuario tenga. Para más info:"
},
"index": {
"status": "Estado",
@@ -399,7 +426,7 @@
"logout": "Cerrar sesión",
"toggleClick": "Abrir con un solo clic",
"toggleDark": "Activar modo oscuro",
"toggleSticky": "Barra lateral visible",
"toggleSticky": "Cambiar visibilidad del panel lateral",
"used": "usado",
"noSources": "No hay fuentes disponibles. Añada una o active defaultEnabled: true."
},
@@ -439,7 +466,7 @@
"allow": "Permitir",
"allowDeny": "Permitir/Denegar",
"userGroup": "Usuario/Grupo",
"enterName": "Introduzca el nombre del usuario o del grupo",
"enterName": "Escriba el nombre del usuario o del grupo",
"deleted": "Regla de acceso eliminada",
"added": "Regla de acceso añadida",
"name": "Nombre",
@@ -447,8 +474,8 @@
"totalDenied": "Denegado",
"totalAllowed": "Permitido",
"all": "Denegar todo",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Muestra lo que ocurre cuando un usuario intenta acceder a una ruta en la que no existen reglas de acceso específicas. Permitir\" significa que los usuarios pueden acceder por defecto, \"Denegar\" significa que los usuarios están bloqueados por defecto a menos que se les permita explícitamente."
"defaultBehaviorDescription": "Muestra lo que ocurre cuando un usuario intenta acceder a una ruta en la que no existen reglas de acceso específicas. Permitir\" significa que los usuarios pueden acceder por defecto, \"Denegar\" significa que los usuarios están bloqueados por defecto a menos que se les permita explícitamente.",
"defaultBehavior": "Acción por defecto{suffix}"
},
"fileLoading": {
"title": "Carga de archivos",
@@ -458,17 +485,17 @@
"uploadChunkSizeMbHelp": "Típicamente 5 a 50 MB, más pequeño es mejor para conexiones inestables. Si se pone a 0, se desactiva la subida de archivo en partes"
},
"colors": {
"red": "rojo",
"green": "verde",
"blue": "azul",
"yellow": "amarillo",
"violet": "violeta",
"orange": "naranja"
"red": "Rojo",
"green": "Verde",
"blue": "Azul",
"yellow": "Amarillo",
"violet": "Morado",
"orange": "Naranja"
},
"profileSettings": {
"autoplayMedia": "Reproducción automática de archivos multimedia",
"autoplayMediaDescription": "Reproducción automática de archivos multimedia al visualizarlos. Esto sólo funcionará si el archivo es un archivo multimedia",
"sidebarOptions": "Opciones de la barra lateral",
"sidebarOptions": "Opciones del panel lateral",
"editorViewerOptions": "Edición y visualización",
"editorQuickSave": "Botón de guardado rápido en el editor",
"editorQuickSaveDescription": "El editor de texto mostrará siempre el icono de guardar, ocultando cualquier otra acción",
@@ -484,9 +511,9 @@
"disableOfficeEditor": "Desactivar el visor y editor de office para las siguientes extensiones:",
"defaultThemeDescription": "Predeterminado",
"customTheme": "Cambiar tema",
"showSelectMultiple": "Mostrar selección múltiple en el menú contextual del escritorio",
"showSelectMultiple": "Mostrar selección múltiple en el menú contextuaL (en PC)",
"showSelectMultipleDescription": "Por defecto, el menú contextual del escritorio no tiene la opción \"seleccionar múltiples\" como el móvil. Esta opción siempre se muestra en el menú contextual.",
"setDateFormat": "Establecer el formato exacto de la fecha",
"setDateFormat": "Usar fecha exacta (DD/MM/AA)",
"showHiddenFiles": "Mostrar archivos ocultos",
"showHiddenFilesDescription": "Muestra los archivos ocultos. (por ejemplo, .thumbnails, ProgramData), pero siguen siendo accesibles manualmente si están configurados para ser indexados.",
"showQuickDownload": "Icono de descarga rápida",
@@ -504,24 +531,34 @@
"popupPreview": "Vista previa emergente",
"popupPreviewDescription": "Se mostrará una ventana emergente superpuesta sobre la interfaz de usuario al pasar el ratón por encima de los archivos que tengan una miniatura",
"filePreviewOptions": "Opciones de previsualización de archivos",
"disableUpdateNotifications": "Desactivar la visualización de notificaciones de actualización",
"disableUpdateNotificationsDescription": "Desactivar el banner de notificación de actualización que aparece para los usuarios administradores en la barra lateral.",
"disableHideSidebar": "Mantener la barra lateral abierta al previsualizar o editar archivos",
"disableHideSidebarDescription": "Si está activada, la barra lateral no se ocultará automáticamente al previsualizar o editar archivos.",
"disableQuickToggles": "Desactivar alternancias rápidas",
"disableQuickTogglesDescription": "Desactiva los toggles rápidos que aparecen en la barra lateral.",
"disableUpdateNotifications": "Desactivar las notificaciones de actualización",
"disableUpdateNotificationsDescription": "Desactivar el banner de notificación de actualización que aparece para los usuarios administradores en el panel lateral.",
"disableHideSidebar": "Mantener el panel lateral abierto al previsualizar o editar archivos",
"disableHideSidebarDescription": "Si está activado, el panel lateral no se ocultará automáticamente al previsualizar o editar archivos.",
"disableQuickToggles": "Desactivar ajustes rápidos",
"disableQuickTogglesDescription": "Desactiva los ajustes rápidos que aparecen en el panel lateral. (cambiar tema, visibilidad del panel y modo de un solo click)",
"disableSearchOptions": "Desactivar las opciones de búsqueda",
"disableSearchOptionsDescription": "Desactivar las opciones de búsqueda que aparecen en la barra lateral.",
"hideSidebarFileActions": "Ocultar el botón de acciones de archivo en la barra lateral",
"disableSearchOptionsDescription": "Desactivar las opciones de búsqueda que aparecen en el panel.",
"hideSidebarFileActions": "Ocultar el botón de acciones de archivo en el panel",
"deleteWithoutConfirming": "Eliminar archivos sin mensaje de confirmación",
"deleteWithoutConfirmingDescription": "Eliminar archivos sin mensaje de confirmación. Esto sólo funcionará si el archivo es un archivo multimedia."
"deleteWithoutConfirmingDescription": "Eliminar archivos sin mensaje de confirmación. Esto sólo funcionará si el archivo es un archivo multimedia.",
"defaultMediaPlayer": "Usar el reproductor multimedia nativo del navegador",
"defaultMediaPlayerDescription": "Usar el reproductor multimedia nativo de tu navegador para los archivos de vídeo y audio en lugar del reproductor multimedia incluido en FileBrowser.",
"debugOfficeEditor": "Activar el modo de depuración de OnlyOffice",
"debugOfficeEditorDescription": "Mostrar una ventana emergente de depuración con información adicional en el editor OnlyOffice.",
"fileViewerOptions": "Opciones del visor de archivos",
"themeAndLanguage": "Tema y lengua"
},
"editor": {
"uninitialized": "Error al inicializar el editor. Por favor, vuelva a cargar.",
"uninitialized": "Error al iniciar el editor, inténtelo de nuevo",
"saveFailed": "No se ha podido guardar el archivo. Por favor, inténtelo de nuevo.",
"saveAborted": "Guardado abortado. El nombre del fichero no coincide con el fichero activo.",
"saveAborted": "No se ha podido guardar el archivo. El nombre del archivo no coincide con el que estás editando.",
"saveDisabled": "No es posible guardar archivos compartidos.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Operación de guardado abortada. El archivo activo de la aplicación ({activeFile}) no coincide con el archivo que está intentando guardar ({tryingToSave}).",
"syncFailed": "No se ha podido sincronizar el estado con la ruta para \"{filename}\" después de 5 intentos. Abortar la configuración del editor para evitar la corrupción de datos."
},
"player": {
"LoopEnabled": "Bucle activado",
"LoopDisabled": "Bucle desactivado"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Effacement terminé",
"showMore": "Afficher plus",
"showLess": "Montrer moins",
"openParentFolder": "Ouvrir le dossier parent"
"openParentFolder": "Ouvrir le dossier parent",
"selectedCount": "Nombre d'éléments sélectionnés pour effectuer une action"
},
"download": {
"downloadFile": "Télécharger le fichier",
@@ -84,15 +85,15 @@
"click": "Sélectionner un élément",
"ctrl": {
"click": "Sélectionner plusieurs éléments",
"f": "Ouvrir l'invite de recherche",
"s": "Télécharger l'élément actuel"
"d": "télécharger les éléments sélectionnés"
},
"del": "Supprimer les éléments sélectionnés",
"doubleClick": "Ouvrir un élément",
"esc": "Désélectionner et/ou fermer la boîte de dialogue",
"f1": "Ouvrir l'aide",
"f2": "Renommer le fichier",
"help": "Aide"
"help": "Aide",
"f1": "show help prompt",
"description": "Vous pouvez consulter les options de navigation de base ci-dessous. Pour plus d'informations, veuillez consulter le site",
"wiki": "Wiki FileBrowser Quantum"
},
"languages": {
"he": "עברית",
@@ -134,16 +135,13 @@
"prompts": {
"copy": "Copier",
"copyMessage": "Choisissez l'emplacement où copier la sélection :",
"deleteMessageMultiple": "Êtes-vous sûr de vouloir supprimer ces {count} élément(s) ?",
"deleteMessageSingle": "Êtes-vous sûr de vouloir supprimer cet élément ?",
"deleteMessageShare": "Êtes-vous sûr de vouloir supprimer ce partage ({path}) ?",
"deleteTitle": "Supprimer",
"displayName": "Nom :",
"download": "Télécharger",
"downloadMessage": "Choisissez le format de téléchargement :",
"error": "Quelque chose s'est mal passé",
"fileInfo": "Informations",
"lastModified": "Dernière modification{suffix}",
"move": "Déplacer",
"moveMessage": "Choisissez l'emplacement où déplacer la sélection :",
"newArchetype": "Créer un nouveau post basé sur un archétype. Votre fichier sera créé dans le dossier de contenu.",
@@ -151,10 +149,7 @@
"newDirMessage": "Nom du nouveau dossier :",
"newFile": "Nouveau fichier",
"newFileMessage": "Nom du nouveau fichier :",
"numberDirs": "Nombre de dossiers{suffix}",
"numberFiles": "Nombre de fichiers{suffix}",
"rename": "Renommer",
"renameMessage": "Nouveau nom pour",
"replace": "Remplacer",
"replaceMessage": "Un des fichiers que vous êtes en train d'importer a le même nom qu'un autre déjà présent. Voulez-vous remplacer le fichier actuel par le nouveau ?\n",
"schedule": "Fixer la date",
@@ -172,10 +167,6 @@
"dragAndDrop": "Glisser-déposer des fichiers ici",
"completed": "Terminé",
"conflictsDetected": "Conflits détectés",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Nom invalide fourni : ne peut contenir \"/\" ou \"\\\".",
"source": "Source{suffix}",
"downloadsLimit": "Limite du nombre de téléchargements",
"maxBandwidth": "Largeur de bande maximale de téléchargement (kbps)",
"shareTheme": "Thème de partage",
@@ -186,22 +177,44 @@
"shareBanner": "Partager l'URL de la bannière",
"shareFavicon": "Partager l'URL de la favicon",
"optionalPasswordHelp": "S'il est défini, les utilisateurs doivent saisir ce mot de passe pour consulter le contenu partagé.",
"viewMode": "Mode d'affichage"
"viewMode": "Mode d'affichage",
"renameMessage": "Saisissez un nouveau nom :",
"renameMessageInvalid": "Un fichier ne peut pas contenir \"/\" ou \"\\\"",
"source": "Source{suffix}",
"deleteMessageMultiple": "Êtes-vous sûr de vouloir supprimer le(s) fichier(s) {count}?",
"deleteMessageShare": "Êtes-vous sûr de vouloir supprimer ce partage : {path} ?",
"lastModified": "Dernière modification{suffix}",
"numberDirs": "Nombre de répertoires{suffix}",
"numberFiles": "Nombre de fichiers{suffix}",
"renameMessageConflict": "Un article nommé \"{filename}\" existe déjà !",
"uploadSettingsChunked": "Nombre maximum de téléchargements simultanés : {maxConcurrentUpload}, taille de chunk : {uploadChunkSizeMb} MO",
"uploadSettingsNoChunk": "Nombre maximum de téléchargements simultanés : {maxConcurrentUpload}, chunked upload désactivé"
},
"search": {
"images": "Images",
"music": "Musique",
"pdf": "PDF",
"pressToSearch": "Appuyez sur entrée pour chercher...",
"search": "Recherche en cours...",
"typeToSearch": "Écrivez pour chercher...",
"types": "Types",
"video": "Vidéo",
"path": "Chemin :",
"smallerThan": "Plus petit que :",
"largerThan": "Plus grand que :",
"helpText1": "La recherche s'effectue sur chaque caractère saisi (3 caractères minimum pour les termes de recherche).",
"helpText2": "Index : La recherche utilise l'index qui est automatiquement mis à jour à l'intervalle configuré (par défaut : 5 minutes). Une recherche effectuée alors que le programme vient de démarrer peut donner des résultats incomplets."
"helpText2": "Index : La recherche utilise l'index qui est automatiquement mis à jour à l'intervalle configuré (par défaut : 5 minutes). Une recherche effectuée alors que le programme vient de démarrer peut donner des résultats incomplets.",
"noResults": "Aucun résultat n'a été trouvé dans la recherche indexée.",
"onlyFolders": "Dossiers uniquement",
"onlyFiles": "Uniquement les fichiers",
"showOptions": "Afficher les options",
"photos": "Photos",
"audio": "Audio",
"videos": "Vidéos",
"documents": "Documents",
"archives": "Archives",
"number": "Nombre",
"searchContext": "Contexte de la recherche : {context}",
"notEnoughCharacters": "Pas assez de caractères pour effectuer une recherche (min {minSearchLength})",
"typeToSearch": "Commencez à taper {minSearchLength} ou plus de caractères pour lancer la recherche."
},
"settings": {
"scopes": "Définir les sources autorisées et la portée de l'utilisateur au sein de celles-ci",
@@ -215,10 +228,8 @@
"avoidChanges": "(Laisser vide pour conserver l'actuel)",
"branding": "Image de marque",
"brandingDirectoryPath": "Chemin du dossier d'image de marque",
"brandingHelp": "Vous pouvez personnaliser l'apparence de votre instance de File Browser en changeant son nom, en remplaçant le logo, en ajoutant des styles personnalisés et même en désactivant les liens externes vers GitHub.\nPour plus d'informations sur la personnalisation de l'image de marque, veuillez consulter la {0}.",
"changePassword": "Modifier le mot de passe",
"commandRunner": "Fichier de commandes",
"commandRunnerHelp": "Ici, vous pouvez définir les commandes qui sont exécutées pour les événements nommés précédemment. Vous devez en écrire une par ligne. Les variables d'environnement {0} et {1} seront disponibles, {0} étant relatif à {1}. Pour plus d'informations sur cette fonctionnalité et les variables d'environnement disponibles, veuillez lire la {2}.",
"commandsUpdated": "Commandes mises à jour !",
"createUserDir": "Créer automatiquement un dossier pour l'utilisateur",
"customStylesheet": "Feuille de style personnalisée",
@@ -300,7 +311,14 @@
"adminOptions": "Options de l'utilisateur administrateur",
"shareDuration": "Expiration",
"searchOptions": "Options de recherche",
"downloads": "Téléchargements"
"downloads": "Téléchargements",
"systemAdmin": "Système et administration",
"configViewer": "Config Viewer",
"configViewerShowFull": "Afficher la configuration complète",
"configViewerShowComments": "Afficher les commentaires",
"configViewerLoadConfig": "Configuration de la charge",
"brandingHelp": "Vous pouvez personnaliser l'apparence de votre instance du navigateur de fichiers en changeant son nom, en remplaçant le logo, en ajoutant des styles personnalisés et même en désactivant les liens externes vers GitHub.\nPour plus d'informations sur la personnalisation de l'image de marque, consultez le site {0}.",
"commandRunnerHelp": "Vous pouvez définir ici les commandes qui seront exécutées dans les événements nommés. Vous devez en écrire une par ligne. Les variables d'environnement {0} et {1} seront disponibles, {0} étant relatif à {1}. Pour plus d'informations sur cette fonctionnalité et les variables d'environnement disponibles, veuillez consulter le site {2}."
},
"sidebar": {
"help": "Aide",
@@ -358,12 +376,21 @@
"hideNavButtons": "Masquer les boutons de navigation",
"hideNavButtonsDescription": "Masquez les boutons de navigation de la barre de navigation dans le partage pour créer une apparence minimaliste.",
"viewModeDescription": "Mise en page par défaut de la page partagée.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Une action vous a été envoyée pour que vous la consultiez ou la téléchargiez.",
"disableShareCard": "Désactiver la carte de partage",
"disableShareCardDescription": "Désactiver la carte de partage sur la page partagée dans la barre latérale ou en haut de la page sur mobile.",
"disableSidebar": "Désactiver la barre latérale",
"disableSidebarDescription": "Désactiver la barre latérale sur la page partagée."
"disableSidebarDescription": "Désactiver la barre latérale sur la page partagée.",
"enforceDarkLightMode": "Renforcer le mode thématique",
"enforceDarkLightModeDescription": "Forcer un mode de thème spécifique (sombre ou clair) pour ce partage, sans tenir compte des préférences de l'utilisateur.",
"default": "Ne pas imposer de mode",
"dark": "Sombre",
"light": "Lumière",
"enableOnlyOffice": "Activer la visionneuse OnlyOffice",
"enableOnlyOfficeDescription": "Autoriser la visualisation de fichiers bureautiques en utilisant OnlyOffice dans ce partage.",
"enableOnlyOfficeEditing": "Activer l'édition OnlyOffice",
"enableOnlyOfficeEditingDescription": "Autoriser l'édition de fichiers bureautiques en utilisant OnlyOffice dans ce partage.",
"titleDefault": "Fichiers partagés - {title}"
},
"api": {
"title": "Clés API :",
@@ -447,8 +474,8 @@
"totalDenied": "Refusé",
"totalAllowed": "Autorisé",
"all": "Refuser tout",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Ceci montre ce qui se passe lorsqu'un utilisateur tente d'accéder à un chemin pour lequel il n'existe pas de règles d'accès spécifiques. La mention \"Autoriser\" signifie que les utilisateurs peuvent accéder par défaut, la mention \"Refuser\" signifie que les utilisateurs sont bloqués par défaut, à moins qu'ils n'y soient explicitement autorisés."
"defaultBehaviorDescription": "Ceci montre ce qui se passe lorsqu'un utilisateur tente d'accéder à un chemin pour lequel il n'existe pas de règles d'accès spécifiques. La mention \"Autoriser\" signifie que les utilisateurs peuvent accéder par défaut, la mention \"Refuser\" signifie que les utilisateurs sont bloqués par défaut, à moins qu'ils n'y soient explicitement autorisés.",
"defaultBehavior": "Action par défaut{suffix}"
},
"fileLoading": {
"title": "Chargements et téléchargements",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - Le thème par défaut",
"customTheme": "Choisissez votre thème",
"showSelectMultiple": "Afficher la sélection multiple dans le menu contextuel sur le bureau",
"showSelectMultipleDescription": "Par défaut, le menu contextuel de l'ordinateur de bureau n'a pas l'option \"sélectionner plusieurs\" comme celui de l'ordinateur mobile. Cette option apparaît toujours dans le menu contextuel."
"showSelectMultipleDescription": "Par défaut, le menu contextuel de l'ordinateur de bureau n'a pas l'option \"sélectionner plusieurs\" comme celui de l'ordinateur mobile. Cette option apparaît toujours dans le menu contextuel.",
"defaultMediaPlayer": "Utilisez le lecteur multimédia natif de votre navigateur",
"defaultMediaPlayerDescription": "Utilisez le lecteur multimédia natif de votre navigateur pour les fichiers vidéo et audio plutôt que le lecteur multimédia \"vue-plyr\" inclus dans FileBrowser.",
"debugOfficeEditor": "Activer le mode de débogage d'OnlyOffice",
"debugOfficeEditorDescription": "Afficher une fenêtre de débogage avec des informations supplémentaires dans l'éditeur OnlyOffice.",
"fileViewerOptions": "Options de visualisation des fichiers",
"themeAndLanguage": "Thème et langue"
},
"editor": {
"uninitialized": "Échec de l'initialisation de l'éditeur. Veuillez recharger.",
"saveFailed": "Échec de l'enregistrement du fichier. Veuillez réessayer.",
"saveAborted": "Sauvegarde interrompue. Le nom du fichier ne correspond pas au fichier actif.",
"saveDisabled": "L'enregistrement de fichiers partagés n'est pas possible.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "L'opération d'enregistrement a été interrompue. Le fichier actif de l'application ({activeFile}) ne correspond pas au fichier que vous essayez d'enregistrer ({tryingToSave}).",
"syncFailed": "n'a pas réussi à synchroniser l'état avec l'itinéraire pour \"{filename}\" après 5 tentatives. Abandon de la configuration de l'éditeur pour éviter la corruption des données."
},
"player": {
"LoopEnabled": "Boucle activée",
"LoopDisabled": "Boucle désactivée"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "השלם",
"showMore": "הצג עוד",
"showLess": "הצג פחות",
"openParentFolder": "פתח את התיקייה הראשית"
"openParentFolder": "פתח את התיקייה הראשית",
"selectedCount": "מספר הפריטים שנבחרו לביצוע פעולה"
},
"download": {
"downloadFile": "הורד קובץ",
@@ -84,15 +85,15 @@
"click": "בחר קובץ או תקייה",
"ctrl": {
"click": "בחר מספר קבצים או תקיות",
"f": "פותח את החיפוש",
"s": "לשמור קובץ או להוריד את התקייה שבה אתה נמצא"
"d": "הורדת פריטים נבחרים"
},
"del": "מחק את מה שנבחר",
"doubleClick": "פתח קובץ או תקייה",
"esc": "נקה את הבחירה ו/או סגור את השדה",
"f1": "המידע הזה",
"f2": "שנה שם קובץ",
"help": "עזרה"
"help": "עזרה",
"f1": "הצג עזרה",
"description": "ניתן לצפות באפשרויות הניווט הבסיסיות להלן. למידע נוסף, בקרו בכתובת",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -134,16 +135,13 @@
"prompts": {
"copy": "העתק",
"copyMessage": "בחר לאן להעתיק את הקבצים:",
"deleteMessageMultiple": "האם אתה בטוח שברצונך למחוק {count} קבצים?",
"deleteMessageSingle": "האם אתה בטוח שברצונך למחוק את הקובץ או התקייה?",
"deleteMessageShare": "האם אתה בטוח שברצונך למחוק את השיתוף הזה?({path})?",
"deleteTitle": "מחק קבצים",
"displayName": "שם:",
"download": "הורד קבצים",
"downloadMessage": "בחר את הפורמט שברצונך להוריד",
"error": "משהו השתבש",
"fileInfo": "מידע על הקובץ",
"lastModified": "שונה לאחרונה{suffix}",
"move": "העבר",
"moveMessage": "בחר מיקום חדש לקובץ / תקייה:",
"newArchetype": "צור פוסט חדש. הקובץ יווצר בתקיית התוכן",
@@ -151,10 +149,7 @@
"newDirMessage": "כתוב את שם התקייה החדשה",
"newFile": "קובץ חדש",
"newFileMessage": "כתוב את שם הקובץ החדש",
"numberDirs": "מספר התקיות{suffix}",
"numberFiles": "מספר הקבצים{suffix}",
"rename": "שנה שם",
"renameMessage": "הכנס שם חדש עבור",
"replace": "החלף",
"replaceMessage": "אחד הקבצים בעל שם זהה לקובץ קיים, האם ברצונך להחליף את הקובץ הקיים בחדש? זהירות - הקובץ הישן ימחק\n",
"schedule": "תזמון",
@@ -172,10 +167,6 @@
"dragAndDrop": "גרור ושחרר קבצים לכאן",
"completed": "הושלם",
"conflictsDetected": "סכסוכים שזוהו",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "שם לא חוקי: לא ניתן להכיל \"/\" או \"\\\"",
"source": "Source{suffix}",
"downloadsLimit": "הגבלת מספר ההורדות",
"maxBandwidth": "רוחב פס הורדה מרבי (kbps)",
"shareTheme": "שתף נושא",
@@ -186,22 +177,44 @@
"shareBanner": "שתף כתובת URL של באנר",
"shareFavicon": "שתף כתובת URL של סמל המועדפים",
"optionalPasswordHelp": "אם אפשרות זו מוגדרת, על המשתמשים להזין סיסמה זו כדי להציג את התוכן המשותף.",
"viewMode": "מצב תצוגה"
"viewMode": "מצב תצוגה",
"renameMessage": "הזן שם חדש:",
"renameMessageInvalid": "קובץ לא יכול להכיל את התווים \"/\" או \"\\\"",
"source": "מקור{suffix}",
"deleteMessageMultiple": "האם אתה בטוח שברצונך למחוק את הקובץ/ים {count}?",
"deleteMessageShare": "האם אתה בטוח שברצונך למחוק שיתוף זה: {path}?",
"lastModified": "שינוי אחרון{suffix}",
"numberDirs": "מספר הספריות{suffix}",
"numberFiles": "מספר הקבצים{suffix}",
"renameMessageConflict": "פריט בשם \"{filename}\" כבר קיים!",
"uploadSettingsChunked": "מספר העלאות מקסימלי בו-זמנית: {maxConcurrentUpload}, גודל קובץ: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "מספר העלאות מקסימלי בו-זמנית: {maxConcurrentUpload}, העלאה מקוטעת מושבתת"
},
"search": {
"images": "תמונות",
"music": "מוזיקה",
"pdf": "PDF",
"pressToSearch": "הקש אנטר לחיפוש...",
"search": "חפש...",
"typeToSearch": "הקלד לחיפוש...",
"types": "סוג",
"video": "וידאו",
"path": "נתיב:",
"smallerThan": "קטן מ:",
"largerThan": "גדול מ:",
"helpText1": "החיפוש מתבצע על כל תו שאתה מקליד (מינימום 3 תווים עבור מונחי חיפוש).",
"helpText2": "האינדקס: החיפוש משתמש באינדקס שמתעדכן אוטומטית במרווח הזמן שהוגדר (ברירת מחדל: 5 דקות). חיפוש בעת הפעלת התוכנית עלול להניב תוצאות חלקיות."
"helpText2": "האינדקס: החיפוש משתמש באינדקס שמתעדכן אוטומטית במרווח הזמן שהוגדר (ברירת מחדל: 5 דקות). חיפוש בעת הפעלת התוכנית עלול להניב תוצאות חלקיות.",
"noResults": "לא נמצאו תוצאות בחיפוש באינדקס.",
"onlyFolders": "רק תיקיות",
"onlyFiles": "רק קבצים",
"showOptions": "הצג אפשרויות",
"photos": "תמונות",
"audio": "אודיו",
"videos": "סרטונים",
"documents": "מסמכים",
"archives": "ארכיונים",
"number": "מספר",
"searchContext": "הקשר החיפוש: {context}",
"notEnoughCharacters": "אין מספיק תווים לחיפוש (מינימום {minSearchLength})",
"typeToSearch": "התחל להקליד {minSearchLength} או יותר תווים כדי להתחיל בחיפוש."
},
"settings": {
"admin": "מנהל",
@@ -214,10 +227,8 @@
"avoidChanges": "(השאר ריק כדי למנוע שינויים)",
"branding": "מיתוג",
"brandingDirectoryPath": "נתיב תקיית מיתוג",
"brandingHelp": "אתה יכול להגדיר את האופן שבו האפליקציה תראה על ידי שינוי שם האפליקציה, החלפת הלוגו, הוספת עיצוב מותאם אישית ואפילו השבתת קישורים חיצוניים לGithub.\nלמידע נוסף עיין ב-{0}.",
"changePassword": "שנה סיסמא",
"commandRunner": "הרצת פקודות",
"commandRunnerHelp": "אתה יכול להגדיר פקודות שיבוצעו באירועים שונים. עליך לכתוב אחד בכל שורה. משתני הסביבה {0} ו-{1} יהיו זמינים, בהיותם {0} ביחס ל-{1}. למידע נוסף על תכונה זו ועל משתני הסביבה הזמינים, עיין ב {2}.",
"commandsUpdated": "הפקודות עודכנו.",
"createUserDir": "צור תקיית בית במהלך הוספת משתמש חדש",
"userHomeBasePath": "נתיב ראשי לתקיות הבית של משתמשים",
@@ -300,7 +311,14 @@
"adminOptions": "אפשרויות משתמש מנהל",
"shareDuration": "תוקף",
"searchOptions": "אפשרויות חיפוש",
"downloads": "הורדות"
"downloads": "הורדות",
"systemAdmin": "מערכת וניהול",
"configViewer": "מציג תצורה",
"configViewerShowFull": "הצג את התצורה המלאה",
"configViewerShowComments": "הצג תגובות",
"configViewerLoadConfig": "טען תצורה",
"brandingHelp": "ניתן להתאים אישית את המראה והתחושה של דפדפן הקבצים על ידי שינוי שמו, החלפת הלוגו, הוספת סגנונות מותאמים אישית ואפילו השבתת קישורים חיצוניים ל-GitHub.\nלמידע נוסף על מיתוג מותאם אישית, בקרו ב- {0}.",
"commandRunnerHelp": "כאן תוכל להגדיר פקודות שיבוצעו באירועים המוזכרים. עליך לכתוב פקודה אחת בכל שורה. המשתנים הסביבתיים {0} ו- {1} יהיו זמינים, והם יהיו {0} ביחס ל- {1}. למידע נוסף על תכונה זו ועל המשתנים הסביבתיים הזמינים, אנא קרא את {2}."
},
"sidebar": {
"help": "עזרה",
@@ -358,12 +376,21 @@
"hideNavButtons": "הסתר כפתורי ניווט",
"hideNavButtonsDescription": "הסתר את כפתורי הניווט בסרגל הניווט בשיתוף כדי ליצור מראה מינימליסטי.",
"viewModeDescription": "פריסת ברירת המחדל עבור הדף המשותף.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "נשלחה אליך קובץ לשיתוף או להורדה.",
"disableShareCard": "השבת כרטיס שיתוף",
"disableShareCardDescription": "השבת את כרטיס השיתוף בדף המשותף בסרגל הצד או בחלק העליון של הדף במכשירים ניידים.",
"disableSidebar": "השבת סרגל צד",
"disableSidebarDescription": "השבת את סרגל הצד בדף המשותף."
"disableSidebarDescription": "השבת את סרגל הצד בדף המשותף.",
"enforceDarkLightMode": "אכוף מצב ערכת נושא",
"enforceDarkLightModeDescription": "כפה מצב ערכת נושא ספציפי (כהה או בהיר) עבור שיתוף זה, תוך עקיפת העדפות המשתמש.",
"default": "אל תכפה מצב",
"dark": "אפל",
"light": "אור",
"enableOnlyOffice": "הפעל את תוכנת הצפייה OnlyOffice",
"enableOnlyOfficeDescription": "אפשר צפייה בקבצי Office באמצעות OnlyOffice בשיתוף זה.",
"enableOnlyOfficeEditing": "אפשר עריכה ב-OnlyOffice",
"enableOnlyOfficeEditingDescription": "אפשר עריכת קבצי Office באמצעות OnlyOffice בשיתוף זה.",
"titleDefault": "קבצים משותפים - {title}"
},
"api": {
"title": "מפתחות API",
@@ -447,8 +474,8 @@
"totalDenied": "נדחה",
"totalAllowed": "מותר",
"all": "דחה הכל",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "זה מראה מה קורה כאשר משתמש מנסה לגשת לנתיב שבו אין כללי גישה ספציפיים. 'אפשר' פירושו שמשתמשים יכולים לגשת כברירת מחדל, 'דחה' פירושו שמשתמשים נחסמים כברירת מחדל, אלא אם כן הותר להם במפורש."
"defaultBehaviorDescription": "זה מראה מה קורה כאשר משתמש מנסה לגשת לנתיב שבו אין כללי גישה ספציפיים. 'אפשר' פירושו שמשתמשים יכולים לגשת כברירת מחדל, 'דחה' פירושו שמשתמשים נחסמים כברירת מחדל, אלא אם כן הותר להם במפורש.",
"defaultBehavior": "פעולה ברירת מחדל{suffix}"
},
"fileLoading": {
"title": "העלאות והורדות",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "ברירת מחדל - ערכת הנושא המוגדרת כברירת מחדל",
"customTheme": "בחר את הנושא שלך",
"showSelectMultiple": "הצג בחירה מרובה בתפריט ההקשר בשולחן העבודה",
"showSelectMultipleDescription": "כברירת מחדל, בתפריט ההקשר של שולחן העבודה אין אפשרות \"בחר מספר פריטים\" כמו במכשירים ניידים. אפשרות זו תמיד מוצגת בתפריט ההקשר."
"showSelectMultipleDescription": "כברירת מחדל, בתפריט ההקשר של שולחן העבודה אין אפשרות \"בחר מספר פריטים\" כמו במכשירים ניידים. אפשרות זו תמיד מוצגת בתפריט ההקשר.",
"defaultMediaPlayer": "השתמש בנגן המדיה המובנה בדפדפן שלך",
"defaultMediaPlayerDescription": "השתמש בנגן המדיה המובנה בדפדפן שלך עבור קבצי וידאו ואודיו, במקום בנגן המדיה 'vue-plyr' הכלול ב-FileBrowser.",
"debugOfficeEditor": "הפעל מצב ניפוי באגים של OnlyOffice",
"debugOfficeEditorDescription": "הצג חלון קופץ לאיתור באגים עם מידע נוסף בעורך OnlyOffice.",
"fileViewerOptions": "אפשרויות תצוגת קבצים",
"themeAndLanguage": "נושא ושפה"
},
"editor": {
"uninitialized": "לא ניתן לאתחל את העורך. אנא טען מחדש.",
"saveFailed": "לא ניתן לשמור את הקובץ. אנא נסה שוב.",
"saveAborted": "שמירה בוטלה. שם הקובץ אינו תואם לקובץ הפעיל.",
"saveDisabled": "שמירת קבצים משותפים אינה נתמכת.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "פעולת השמירה בוטלה. הקובץ הפעיל של היישום ({activeFile}) אינו תואם לקובץ שאתה מנסה לשמור ({tryingToSave}).",
"syncFailed": "לא הצליח לסנכרן את המצב עם המסלול עבור \"{filename}\" לאחר 5 ניסיונות. מבטל את הגדרת העורך כדי למנוע פגיעה בנתונים."
},
"player": {
"LoopEnabled": "לולאה מופעלת",
"LoopDisabled": "לולאה מושבתת"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Tiszta befejezve",
"showMore": "Többet mutasson",
"showLess": "Kevesebb mutatása",
"openParentFolder": "Szülői mappa megnyitása"
"openParentFolder": "Szülői mappa megnyitása",
"selectedCount": "A művelet végrehajtásához kiválasztott elemek száma"
},
"download": {
"downloadFile": "Fájl letöltése",
@@ -84,15 +85,15 @@
"click": "mappa vagy fájl kijelölése",
"ctrl": {
"click": "több mappa vagy fájl kijelölése",
"f": "keresés megnyitása",
"s": "az aktuális fájl vagy mappa letöltése"
"d": "kiválasztott elemek letöltése"
},
"del": "kijelölt elemek törlése",
"doubleClick": "fájl vagy mappa megnyitása",
"esc": "kijelölés törlése és/vagy parancssor bezárása",
"f1": "ezen információ megjelenítése",
"f2": "fájl átnevezése",
"help": "Súgó"
"help": "Súgó",
"f1": "show help prompt",
"description": "Az alábbiakban megtekintheti az alapvető navigációs lehetőségeket. További információkért kérjük, látogasson el a következő weboldalra",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -134,16 +135,13 @@
"prompts": {
"copy": "Másolása",
"copyMessage": "Válassza ki a másolás célját:",
"deleteMessageMultiple": "Biztosan törölni szeretne {count} fájlt?",
"deleteMessageSingle": "Biztosan törölni szeretné ezt a fájl vagy mappát?",
"deleteMessageShare": "Biztosan törölni szeretné ezt a megosztást ({path})?",
"deleteTitle": "Fájlok törlése",
"displayName": "Megjelenített név:",
"download": "Fájlok letöltése",
"downloadMessage": "Válassza ki a letöltés formátumát.",
"error": "Valami rosszul sült el",
"fileInfo": "Fájlinformáció",
"lastModified": "Utolsó módosítás{suffix}",
"move": "Mozgatás",
"moveMessage": "Válasszon új helyet a fájl(ok)nak/mappá(k)nak:",
"newArchetype": "Új bejegyzést hoz létre egy archetípus alapján. A fájl a tartalom mappában jön létre.",
@@ -151,10 +149,7 @@
"newDirMessage": "Adja meg az új mappa nevét.",
"newFile": "Új fájl",
"newFileMessage": "Adja meg az új fájl nevét.",
"numberDirs": "Mappák száma{suffix}",
"numberFiles": "Fájlok száma{suffix}",
"rename": "Átnevezés",
"renameMessage": "Adja meg az új nevét:",
"replace": "Csere",
"replaceMessage": "Az egyik feltölteni kívánt fájl a neve miatt ütközik. Szeretné lecserélni a meglévő fájlt?\n",
"schedule": "Ütemezés",
@@ -172,10 +167,6 @@
"dragAndDrop": "Húzza ide a fájlokat",
"completed": "Befejezett",
"conflictsDetected": "Feltárt konfliktusok",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Érvénytelen név: nem lehet \"/\" vagy \"\\\" konténer",
"source": "Source{suffix}",
"downloadsLimit": "Letöltések száma limit",
"maxBandwidth": "Maximális letöltési sávszélesség (kbps)",
"shareTheme": "Megosztás témája",
@@ -186,22 +177,44 @@
"shareBanner": "Banner URL megosztása",
"shareFavicon": "Favicon URL megosztása",
"optionalPasswordHelp": "Ha be van állítva, a felhasználóknak ezt a jelszót kell megadniuk a megosztott tartalom megtekintéséhez.",
"viewMode": "Nézet mód"
"viewMode": "Nézet mód",
"renameMessage": "Adjon meg egy új nevet:",
"renameMessageInvalid": "Egy fájl nem tartalmazhat \"/\" vagy \"\\\" konténert.",
"source": "Forrás{suffix}",
"deleteMessageMultiple": "Biztos, hogy törölni szeretné a {count} fájl(ok)at?",
"deleteMessageShare": "Biztos, hogy törölni szeretné ezt a megosztást: {path} ?",
"lastModified": "Utoljára módosítva{suffix}",
"numberDirs": "A könyvtárak száma{suffix}",
"numberFiles": "Fájlok száma{suffix}",
"renameMessageConflict": "A \"{filename}\" nevű elem már létezik!",
"uploadSettingsChunked": "Maximális egyidejű feltöltés: {maxConcurrentUpload}, chunk méret: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Maximális egyidejű feltöltés: {maxConcurrentUpload}, chunked upload disabled"
},
"search": {
"images": "Képek",
"music": "Zene",
"pdf": "PDF",
"pressToSearch": "Keresés indítása Enterrel…",
"search": "Keresés…",
"typeToSearch": "Keresés indítása beírással…",
"types": "Típusok",
"video": "Videó",
"path": "Útvonal:",
"smallerThan": "Kisebb, mint:",
"largerThan": "Nagyobb, mint:",
"helpText1": "A keresés minden egyes beírt karakterre történik (minimum 3 karakteres keresőkifejezés).",
"helpText2": "Az index: A keresés az indexet használja, amely automatikusan frissül a beállított időközönként (alapértelmezett: 5 perc). A program indulásakor végzett keresés hiányos eredményeket eredményezhet."
"helpText2": "Az index: A keresés az indexet használja, amely automatikusan frissül a beállított időközönként (alapértelmezett: 5 perc). A program indulásakor végzett keresés hiányos eredményeket eredményezhet.",
"noResults": "Az indexelt keresésben nem találtak találatot.",
"onlyFolders": "Csak mappák",
"onlyFiles": "Csak fájlok",
"showOptions": "Beállítások megjelenítése",
"photos": "Fotók",
"audio": "Audio",
"videos": "Videók",
"documents": "Dokumentumok",
"archives": "Archívum",
"number": "Szám",
"searchContext": "Keresési kontextus: {context}",
"notEnoughCharacters": "Nincs elég karakter a kereséshez (min {minSearchLength})",
"typeToSearch": "A keresés megkezdéséhez írja be a {minSearchLength} vagy több karaktert."
},
"settings": {
"admin": "Admin",
@@ -214,10 +227,8 @@
"avoidChanges": "(üresen hagyva nincs változás)",
"branding": "Márkázás",
"brandingDirectoryPath": "Márkázás mappaútvonala",
"brandingHelp": "Testre szabhatja a File Browser példányának megjelenését a név megváltoztatásával, a logó cseréjével, egyéni stílusok hozzáadásával és még a GitHubra mutató külső hivatkozások letiltásával is.\nAz egyéni márkázással kapcsolatos további információkért tekintse meg: {0}.",
"changePassword": "Jelszó módosítása",
"commandRunner": "Parancsfuttató",
"commandRunnerHelp": "Beállíthatja azokat a parancsokat, amelyek a megnevezett események során végrehajtásra kerülnek. Soronként egyet kell megadni. A {0} és a {1} környezeti változók lesznek elérhetőek, ahol a {0} relatív a {1}-hez. A funkcióról és a rendelkezésre álló környezeti változókról további információ: {2}.",
"commandsUpdated": "Parancsok frissítve!",
"createUserDir": "Felhasználók saját mappáinak automatikus létrehozása új felhasználók hozzáadásakor",
"userHomeBasePath": "Alap elérési útvonal a felhasználók saját mappáihoz",
@@ -300,7 +311,14 @@
"adminOptions": "Admin felhasználói beállítások",
"shareDuration": "Lejárat:",
"searchOptions": "Keresési lehetőségek",
"downloads": "Letöltések"
"downloads": "Letöltések",
"systemAdmin": "Rendszer és adminisztráció",
"configViewer": "Konfiguráció megtekintő",
"configViewerShowFull": "Teljes konfiguráció megjelenítése",
"configViewerShowComments": "Hozzászólások megjelenítése",
"configViewerLoadConfig": "Konfiguráció betöltése",
"brandingHelp": "Testreszabhatja a Fájlböngésző példányának megjelenését és érzetét a név megváltoztatásával, a logó cseréjével, egyéni stílusok hozzáadásával és akár a GitHubra mutató külső hivatkozások letiltásával.\nAz egyéni márkaépítéssel kapcsolatos további információkat a {0} oldalon talál.",
"commandRunnerHelp": "Itt állíthatja be azokat a parancsokat, amelyek a megnevezett események során végrehajtásra kerülnek. Soronként egyet kell írni. A {0} és a {1} környezeti változók lesznek elérhetőek, a {0} a {1}-hoz képest . A funkcióról és a rendelkezésre álló környezeti változókról bővebb információt a {2} oldalon talál."
},
"sidebar": {
"help": "Súgó",
@@ -358,12 +376,21 @@
"hideNavButtons": "Navigációs gombok elrejtése",
"hideNavButtonsDescription": "Rejtse el a navigációs gombokat a megosztás navigációs sávján a minimalista megjelenés érdekében.",
"viewModeDescription": "A megosztott oldal alapértelmezett elrendezése.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Megosztást küldtünk Önnek megtekintésre vagy letöltésre.",
"disableShareCard": "Megosztási kártya letiltása",
"disableShareCardDescription": "Kapcsolja ki a megosztási kártyát a megosztott oldalon az oldalsávban vagy mobilon az oldal tetején.",
"disableSidebar": "Oldalsáv kikapcsolása",
"disableSidebarDescription": "Kapcsolja ki az oldalsávot a megosztott oldalon."
"disableSidebarDescription": "Kapcsolja ki az oldalsávot a megosztott oldalon.",
"enforceDarkLightMode": "Témamód érvényesítése",
"enforceDarkLightModeDescription": "Egy adott témamód (sötét vagy világos) kikényszerítése a megosztás számára, felülírva a felhasználói beállításokat.",
"default": "Ne kényszerítsen ki üzemmódot",
"dark": "Sötét",
"light": "Fény",
"enableOnlyOffice": "OnlyOffice megjelenítő engedélyezése",
"enableOnlyOfficeDescription": "Engedélyezze az irodai fájlok megtekintését az OnlyOffice használatával ezen a megosztáson.",
"enableOnlyOfficeEditing": "Engedélyezze az OnlyOffice szerkesztést",
"enableOnlyOfficeEditingDescription": "Engedélyezze az irodai fájlok szerkesztését az OnlyOffice használatával ezen a megosztáson.",
"titleDefault": "Megosztott fájlok - {title}"
},
"api": {
"title": "API-kulcsok:",
@@ -447,8 +474,8 @@
"totalDenied": "Elutasítva",
"totalAllowed": "Engedélyezett",
"all": "Mindent megtagadni",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Ez azt mutatja, hogy mi történik, ha egy felhasználó megpróbál hozzáférni egy olyan elérési útvonalhoz, ahol nincsenek speciális hozzáférési szabályok. Az 'Allow' azt jelenti, hogy a felhasználók alapértelmezés szerint hozzáférhetnek, a 'Deny' azt jelenti, hogy a felhasználók alapértelmezés szerint blokkolva vannak, kivéve, ha kifejezetten engedélyezik."
"defaultBehaviorDescription": "Ez azt mutatja, hogy mi történik, ha egy felhasználó megpróbál hozzáférni egy olyan elérési útvonalhoz, ahol nincsenek speciális hozzáférési szabályok. Az 'Allow' azt jelenti, hogy a felhasználók alapértelmezés szerint hozzáférhetnek, a 'Deny' azt jelenti, hogy a felhasználók alapértelmezés szerint blokkolva vannak, kivéve, ha kifejezetten engedélyezik.",
"defaultBehavior": "Alapértelmezett cselekvés{suffix}"
},
"fileLoading": {
"title": "Feltöltések és letöltések",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - Az alapértelmezett téma",
"customTheme": "Válasszon témát",
"showSelectMultiple": "Többszörös kijelölés megjelenítése az asztali kontextusmenüben",
"showSelectMultipleDescription": "Alapértelmezés szerint az asztali kontextusmenü nem rendelkezik a \"többszörös kiválasztás\" opcióval, mint a mobilon. Ez az opció mindig megjelenik a kontextusmenüben."
"showSelectMultipleDescription": "Alapértelmezés szerint az asztali kontextusmenü nem rendelkezik a \"többszörös kiválasztás\" opcióval, mint a mobilon. Ez az opció mindig megjelenik a kontextusmenüben.",
"defaultMediaPlayer": "Használja a böngésző natív médialejátszóját",
"defaultMediaPlayerDescription": "Használja a böngésző natív médialejátszóját a videó- és hangfájlokhoz, nem pedig a FileBrowser mellékelt \"vue-plyr\" médialejátszóját.",
"debugOfficeEditor": "OnlyOffice hibakeresési mód engedélyezése",
"debugOfficeEditorDescription": "További információkat tartalmazó hibakeresési felugró ablak megjelenítése az OnlyOffice szerkesztőben.",
"fileViewerOptions": "Fájlnéző beállításai",
"themeAndLanguage": "Téma és nyelv"
},
"editor": {
"uninitialized": "Nem sikerült inicializálni a szerkesztőt. Kérjük, töltse újra.",
"saveFailed": "A fájl mentése sikertelen. Kérjük, próbálja újra.",
"saveAborted": "A mentés megszakadt. A fájl neve nem egyezik az aktív fájléval.",
"saveDisabled": "A megosztott fájlok mentése nem támogatott.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "A mentési művelet megszakadt. Az alkalmazás aktív fájlja ({activeFile}) nem egyezik meg a menteni kívánt fájllal ({tryingToSave}).",
"syncFailed": "5 próbálkozás után nem sikerült szinkronizálni az állapotot a \"{filename}\" útvonallal. A szerkesztő beállításának megszakítása az adatok sérülésének elkerülése érdekében."
},
"player": {
"LoopEnabled": "Hurok engedélyezve",
"LoopDisabled": "Hurok letiltva"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Completato il lavoro di pulizia",
"showMore": "Mostra di più",
"showLess": "Mostra meno",
"openParentFolder": "Aprire la cartella madre"
"openParentFolder": "Aprire la cartella madre",
"selectedCount": "Numero di elementi selezionati per eseguire un'azione"
},
"download": {
"downloadFile": "Scarica file",
@@ -84,15 +85,15 @@
"click": "seleziona un file o una cartella",
"ctrl": {
"click": "seleziona più file o cartelle",
"f": "apre la barra di ricerca",
"s": "salva un file o scarica la cartella in cui ci si trova"
"d": "scaricare gli articoli selezionati"
},
"del": "elimina gli elementi selezionati",
"doubleClick": "apre un file o una cartella",
"esc": "annulla la selezione e/o chiude la finestra aperta",
"f1": "questo pannello",
"f2": "rinomina un file",
"help": "Aiuto"
"help": "Aiuto",
"f1": "mostra la richiesta di aiuto",
"description": "Le opzioni di navigazione di base sono riportate di seguito. Per ulteriori informazioni, visitate il sito",
"wiki": "Wiki di FileBrowser Quantum"
},
"languages": {
"he": "עברית",
@@ -134,7 +135,6 @@
"prompts": {
"copy": "Copia",
"copyMessage": "Seleziona la cartella in cui copiare i file:",
"deleteMessageMultiple": "Sei sicuro di voler eliminare {count} file?",
"deleteMessageSingle": "Sei sicuro di voler eliminare questo file/cartella?",
"deleteTitle": "Elimina",
"displayName": "Nome visualizzato:",
@@ -142,7 +142,6 @@
"downloadMessage": "Seleziona il formato che vuoi scaricare.",
"error": "Qualcosa è andato per il verso storto",
"fileInfo": "Informazioni sul file",
"lastModified": "Ultima modifica{suffix}",
"move": "Sposta",
"moveMessage": "Seleziona la nuova posizione per i tuoi file e/o cartella/e:",
"newArchetype": "Crea un nuovo post basato su un modello. Il tuo file verrà creato nella cartella.",
@@ -150,10 +149,7 @@
"newDirMessage": "Scrivi il nome della nuova cartella.",
"newFile": "Nuovo file",
"newFileMessage": "Scrivi il nome del nuovo file.",
"numberDirs": "Numero di cartelle{suffix}",
"numberFiles": "Numero di file{suffix}",
"rename": "Rinomina",
"renameMessage": "Inserisci un nuovo nome per",
"replace": "Sostituisci",
"replaceMessage": "Uno dei file che stai cercando di caricare sta generando un conflitto per via del suo nome. Desideri sostituire il file già esistente?\n",
"schedule": "Pianifica",
@@ -161,7 +157,6 @@
"show": "Mostra",
"size": "Dimensione",
"upload": "Carica",
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
"deleteUserMessage": "Siete sicuri di voler eliminare questo utente?",
"filesSelected": "file selezionati",
"optionalPassword": "Password opzionale",
@@ -172,10 +167,6 @@
"dragAndDrop": "Trascinare e rilasciare i file qui",
"completed": "Completato",
"conflictsDetected": "Conflitti rilevati",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Nome non valido: non può contenere \"/\" o \"\\\".",
"source": "Source{suffix}",
"downloadsLimit": "Limite del numero di download",
"maxBandwidth": "Larghezza di banda massima per il download (kbps)",
"shareTheme": "Tema della condivisione",
@@ -186,22 +177,44 @@
"shareBanner": "Condividi l'URL del banner",
"shareFavicon": "Condividere l'URL della favicon",
"optionalPasswordHelp": "Se impostata, gli utenti devono inserire questa password per visualizzare il contenuto condiviso.",
"viewMode": "Modalità di visualizzazione"
"viewMode": "Modalità di visualizzazione",
"renameMessage": "Inserire un nuovo nome:",
"renameMessageInvalid": "Un file non può contenere \"/\" o \"\\\".",
"source": "Fonte{suffix}",
"deleteMessageMultiple": "Siete sicuri di voler eliminare i file {count}?",
"deleteMessageShare": "Sei sicuro di voler eliminare questa condivisione: {path} ?",
"lastModified": "Ultima modifica{suffix}",
"numberDirs": "Numero di directory{suffix}",
"numberFiles": "Numero di file{suffix}",
"renameMessageConflict": "Un elemento chiamato \"{filename}\" esiste già!",
"uploadSettingsChunked": "Carichi massimi contemporanei: {maxConcurrentUpload}, dimensione dei pezzi: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Carichi massimi contemporanei: {maxConcurrentUpload}, caricamento chunked disabilitato"
},
"search": {
"images": "Immagini",
"music": "Musica",
"pdf": "PDF",
"pressToSearch": "Premi Invio per cercare...",
"search": "Cerca...",
"typeToSearch": "Scrivi per cercare...",
"types": "Tipi",
"video": "Video",
"path": "Percorso:",
"smallerThan": "Più piccolo di:",
"largerThan": "Più grande di:",
"helpText1": "La ricerca avviene per ogni carattere digitato (minimo 3 caratteri per i termini di ricerca).",
"helpText2": "L'indice: La ricerca utilizza l'indice che viene aggiornato automaticamente all'intervallo configurato (default: 5 minuti). La ricerca quando il programma è appena stato avviato può dare risultati incompleti."
"helpText2": "L'indice: La ricerca utilizza l'indice che viene aggiornato automaticamente all'intervallo configurato (default: 5 minuti). La ricerca quando il programma è appena stato avviato può dare risultati incompleti.",
"noResults": "Nessun risultato trovato nella ricerca indicizzata.",
"onlyFolders": "Solo cartelle",
"onlyFiles": "Solo file",
"showOptions": "Mostra opzioni",
"photos": "Foto",
"audio": "Audio",
"videos": "Video",
"documents": "Documenti",
"archives": "Archivio",
"number": "Numero",
"searchContext": "Contesto di ricerca: {context}",
"notEnoughCharacters": "Non ci sono abbastanza caratteri per la ricerca (min {minSearchLength})",
"typeToSearch": "Iniziare a digitare {minSearchLength} o più caratteri per avviare la ricerca."
},
"settings": {
"admin": "Admin",
@@ -214,10 +227,8 @@
"avoidChanges": "(lascia vuoto per evitare cambiamenti)",
"branding": "Branding",
"brandingDirectoryPath": "Directory del branding",
"brandingHelp": "Puoi personalizzare l'aspetto e il comportamento di File Browser cambiando il suo nome, logo, aggiungendo stili personalizzati e anche disabilitando link esterni verso GitHub.\nPer altre informazioni sul branding personalizzato, vai alla {0}.",
"changePassword": "Modifica password",
"commandRunner": "Esecutore di comandi",
"commandRunnerHelp": "Qui puoi impostare i comandi da eseguire negli eventi nominati. Ne devi scrivere uno per riga. Le variabili d'ambiente {0} e {1} sono disponibili, essendo {0} relativo a {1}. Per altre informazioni su questa funzionalità e sulle variabili d'ambiente utilizzabili, leggi la {2}.",
"commandsUpdated": "Comandi aggiornati!",
"createUserDir": "Crea automaticamente la home directory dell'utente quando lo aggiungi",
"customStylesheet": "Foglio di stile personalizzato",
@@ -300,7 +311,14 @@
"adminOptions": "Opzioni dell'utente amministratore",
"shareDuration": "Scadenza",
"searchOptions": "Opzioni di ricerca",
"downloads": "Scaricamento"
"downloads": "Scaricamento",
"systemAdmin": "Sistema e amministrazione",
"configViewer": "Visualizzatore di configurazione",
"configViewerShowFull": "Mostra la configurazione completa",
"configViewerShowComments": "Mostra i commenti",
"configViewerLoadConfig": "Configurazione del carico",
"brandingHelp": "È possibile personalizzare l'aspetto dell'istanza del File Browser cambiandone il nome, sostituendo il logo, aggiungendo stili personalizzati e persino disabilitando i collegamenti esterni a GitHub.\nPer ulteriori informazioni sul branding personalizzato, consultare il sito {0}.",
"commandRunnerHelp": "Qui è possibile impostare i comandi che vengono eseguiti negli eventi denominati. È necessario scriverne uno per riga. Le variabili d'ambiente {0} e {1} saranno disponibili, essendo {0} relativa a {1}. Per ulteriori informazioni su questa funzione e sulle variabili d'ambiente disponibili, leggere {2}."
},
"sidebar": {
"help": "Aiuto",
@@ -358,12 +376,21 @@
"hideNavButtons": "Nascondere i pulsanti di navigazione",
"hideNavButtonsDescription": "Nascondere i pulsanti di navigazione sulla navbar nella condivisione per creare un look minimalista.",
"viewModeDescription": "Layout predefinito per la pagina condivisa.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "È stata inviata una condivisione da visualizzare o scaricare.",
"disableShareCard": "Disattivare la scheda di condivisione",
"disableShareCardDescription": "Disattivare la scheda di condivisione sulla pagina condivisa nella barra laterale o nella parte superiore della pagina su mobile.",
"disableSidebar": "Disattivare la barra laterale",
"disableSidebarDescription": "Disattivare la barra laterale della pagina condivisa."
"disableSidebarDescription": "Disattivare la barra laterale della pagina condivisa.",
"enforceDarkLightMode": "Applicare la modalità a tema",
"enforceDarkLightModeDescription": "Forza una modalità tema specifica (scuro o chiaro) per questa condivisione, ignorando le preferenze dell'utente.",
"default": "Non applicare una modalità",
"dark": "Scuro",
"light": "Luce",
"enableOnlyOffice": "Abilitate il visualizzatore OnlyOffice",
"enableOnlyOfficeDescription": "Consente di visualizzare i file di Office utilizzando OnlyOffice in questa condivisione.",
"enableOnlyOfficeEditing": "Abilita la modifica di OnlyOffice",
"enableOnlyOfficeEditingDescription": "Consentire la modifica dei file di Office utilizzando OnlyOffice in questa condivisione.",
"titleDefault": "File condivisi - {title}"
},
"api": {
"title": "Chiavi API:",
@@ -447,8 +474,8 @@
"totalDenied": "Negato",
"totalAllowed": "Consentito",
"all": "Rifiuta tutto",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Questo mostra cosa succede quando un utente cerca di accedere a un percorso per il quale non esistono regole di accesso specifiche. 'Consenti' significa che gli utenti possono accedere per impostazione predefinita, 'Rifiuta' significa che gli utenti sono bloccati per impostazione predefinita, a meno che non siano esplicitamente autorizzati."
"defaultBehaviorDescription": "Questo mostra cosa succede quando un utente cerca di accedere a un percorso per il quale non esistono regole di accesso specifiche. 'Consenti' significa che gli utenti possono accedere per impostazione predefinita, 'Rifiuta' significa che gli utenti sono bloccati per impostazione predefinita, a meno che non siano esplicitamente autorizzati.",
"defaultBehavior": "Azione predefinita{suffix}"
},
"fileLoading": {
"title": "Caricamenti e download",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - Il tema predefinito",
"customTheme": "Scegliete il vostro tema",
"showSelectMultiple": "Mostra selezione multipla nel menu contestuale sul desktop",
"showSelectMultipleDescription": "Per impostazione predefinita, il menu contestuale del desktop non dispone dell'opzione \"seleziona più\" come quello del cellulare. Questa opzione viene sempre visualizzata nel menu contestuale."
"showSelectMultipleDescription": "Per impostazione predefinita, il menu contestuale del desktop non dispone dell'opzione \"seleziona più\" come quello del cellulare. Questa opzione viene sempre visualizzata nel menu contestuale.",
"defaultMediaPlayer": "Utilizzare il lettore multimediale nativo del browser",
"defaultMediaPlayerDescription": "Utilizzate il lettore multimediale nativo del vostro browser per i file video e audio piuttosto che il lettore multimediale \"vue-plyr\" incluso in FileBrowser.",
"debugOfficeEditor": "Abilita la modalità di debug di OnlyOffice",
"debugOfficeEditorDescription": "Mostra un popup di debug con informazioni aggiuntive nell'editor di OnlyOffice.",
"fileViewerOptions": "Opzioni del visualizzatore di file",
"themeAndLanguage": "Tema e linguaggio"
},
"editor": {
"uninitialized": "Impossibile inizializzare l'editor. Ricaricare.",
"saveFailed": "Impossibile salvare il file. Riprovare.",
"saveAborted": "Salvataggio interrotto. Il nome del file non corrisponde al file attivo.",
"saveDisabled": "Il salvataggio di file condivisi non è supportato.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Operazione di salvataggio interrotta. Il file attivo dell'applicazione ({activeFile}) non corrisponde al file che si sta cercando di salvare ({tryingToSave}).",
"syncFailed": "non è riuscito a sincronizzare lo stato con il percorso per \"{filename}\" dopo 5 tentativi. Interruzione dell'impostazione dell'editor per evitare la corruzione dei dati."
},
"player": {
"LoopEnabled": "Loop abilitato",
"LoopDisabled": "Loop disabilitato"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "クリア完了",
"showMore": "もっと見る",
"showLess": "もっと見る",
"openParentFolder": "親フォルダを開く"
"openParentFolder": "親フォルダを開く",
"selectedCount": "アクションを実行するために選択された項目の数"
},
"download": {
"downloadFile": "Download File",
@@ -84,15 +85,15 @@
"click": "ファイルやディレクトリを選択",
"ctrl": {
"click": "複数のファイルやディレクトリを選択",
"f": "検索を有効にする",
"s": "ファイルを保存またはカレントディレクトリをダウンロード"
"d": "ダウンロード"
},
"del": "選択した項目を削除",
"doubleClick": "ファイルやディレクトリをオープン",
"esc": "選択をクリアーまたはプロンプトを閉じる",
"f1": "このヘルプを表示",
"f2": "ファイルの名前を変更",
"help": "ヘルプ"
"help": "ヘルプ",
"f1": "ヘルププロンプトを表示する",
"description": "基本的なナビオプションは以下からご覧いただけます。その他の情報については",
"wiki": "ファイルブラウザ クォンタム ウィキ"
},
"languages": {
"he": "עברית",
@@ -134,7 +135,6 @@
"prompts": {
"copy": "コピー",
"copyMessage": "コピーの目標ディレクトリを選択してください:",
"deleteMessageMultiple": "{count} つのファイルを本当に削除してよろしいですか。",
"deleteMessageSingle": "このファイル/フォルダを本当に削除してよろしいですか。",
"deleteTitle": "ファイルを削除",
"displayName": "名前:",
@@ -142,7 +142,6 @@
"downloadMessage": "圧縮形式を選択してください。",
"error": "あるエラーが発生しました。",
"fileInfo": "ファイル情報",
"lastModified": "最終変更{suffix}",
"move": "移動",
"moveMessage": "移動の目標ディレクトリを選択してください:",
"newArchetype": "ある元型に基づいて新しいポストを作成します。ファイルは コンテンツフォルダに作成されます。",
@@ -150,10 +149,7 @@
"newDirMessage": "新しいディレクトリの名前を入力してください。",
"newFile": "新しいファイルを作成",
"newFileMessage": "新しいファイルの名前を入力してください。",
"numberDirs": "ディレクトリ個数{suffix}",
"numberFiles": "ファイル個数{suffix}",
"rename": "名前を変更",
"renameMessage": "名前を変更しようファイルは:",
"replace": "置き換える",
"replaceMessage": "アップロードするファイルの中でかち合う名前が一つあります。 既存のファイルを置き換えりませんか。\n",
"schedule": "スケジュール",
@@ -161,7 +157,6 @@
"show": "表示",
"size": "サイズ",
"upload": "アップロード",
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
"deleteUserMessage": "本当にこのユーザーを削除しますか?",
"filesSelected": "選択されたファイル",
"optionalPassword": "任意のパスワード",
@@ -172,10 +167,6 @@
"dragAndDrop": "ここにファイルをドラッグ&ドロップする",
"completed": "完成",
"conflictsDetected": "コンフリクト検出",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "無効な名前が指定されました:\"/\"または\"˶\"をコンテナに入れることはできません。",
"source": "Source{suffix}",
"downloadsLimit": "ダウンロード数制限",
"maxBandwidth": "最大ダウンロード帯域幅 (kbps)",
"shareTheme": "シェアテーマ",
@@ -186,22 +177,44 @@
"shareBanner": "共有バナーURL",
"shareFavicon": "ファビコンのURLを共有する",
"optionalPasswordHelp": "設定されている場合、ユーザーは共有コンテンツを見るためにこのパスワードを入力しなければならない。",
"viewMode": "ビューモード"
"viewMode": "ビューモード",
"renameMessage": "新しい名前を入力する:",
"renameMessageInvalid": "ファイルには\"/\"や\"˶\"を入れることはできません。",
"source": "ソース{suffix}",
"deleteMessageMultiple": "本当に{count} ファイルを削除しますか?",
"deleteMessageShare": "この共有を削除してもよろしいですか? {path} ?",
"lastModified": "最終更新日{suffix}",
"numberDirs": "ディレクトリ数{suffix}",
"numberFiles": "ファイル数{suffix}",
"renameMessageConflict": "{filename}\" という名前のアイテムがすでに存在する!",
"uploadSettingsChunked": "最大同時アップロード数{maxConcurrentUpload}チャンクサイズ {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "最大同時アップロード数{maxConcurrentUpload}チャンクアップロード無効"
},
"search": {
"images": "画像",
"music": "音楽",
"pdf": "PDF",
"pressToSearch": "Press enter to search...",
"search": "検索...",
"typeToSearch": "Type to search...",
"types": "種類",
"video": "ビデオ",
"path": "パス",
"smallerThan": "より小さい:",
"largerThan": "より大きい:",
"helpText1": "検索は入力した文字ごとに行われます(検索語は最低3文字)。",
"helpText2": "インデックス:検索は、設定された間隔(デフォルト:5分)で自動的に更新されるインデックスを利用します。プログラムが開始したばかりの時に検索すると、不完全な結果になることがあります。"
"helpText2": "インデックス:検索は、設定された間隔(デフォルト:5分)で自動的に更新されるインデックスを利用します。プログラムが開始したばかりの時に検索すると、不完全な結果になることがあります。",
"noResults": "インデックス検索で結果が見つかりませんでした。",
"onlyFolders": "フォルダのみ",
"onlyFiles": "ファイルのみ",
"showOptions": "オプションの表示",
"photos": "写真",
"audio": "オーディオ",
"videos": "ビデオ",
"documents": "資料",
"archives": "アーカイブス",
"number": "番号",
"searchContext": "検索コンテキスト:{context}",
"notEnoughCharacters": "検索する文字数が足りない (最小値{minSearchLength})",
"typeToSearch": "検索を開始するには、{minSearchLength} 以上の文字を入力します。"
},
"settings": {
"admin": "管理者",
@@ -214,10 +227,8 @@
"avoidChanges": "(変更を避けるために空白にしてください)",
"branding": "Branding",
"brandingDirectoryPath": "Branding directory path",
"brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
"changePassword": "パスワードを変更",
"commandRunner": "Command runner",
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "コマンドは更新されました!",
"createUserDir": "Auto create user home dir while adding new user",
"customStylesheet": "カスタムスタイルシ ート",
@@ -300,7 +311,14 @@
"adminOptions": "管理者ユーザーオプション",
"shareDuration": "期限切れ",
"searchOptions": "検索オプション",
"downloads": "ダウンロード"
"downloads": "ダウンロード",
"systemAdmin": "システム&管理",
"configViewer": "コンフィグビューア",
"configViewerShowFull": "フルコンフィグを表示",
"configViewerShowComments": "コメントを表示",
"configViewerLoadConfig": "ロードコンフィグ",
"brandingHelp": "ファイルブラウザのインスタンスの見た目や使い勝手は、名前の変更、ロゴの置き換え、カスタムスタイルの追加、GitHub への外部リンクの無効化などでカスタマイズできます。\nカスタムブランディングの詳細については、{0} をご覧ください。",
"commandRunnerHelp": "ここでは、指定したイベントで実行されるコマンドを設定することができる。1行に1つ書く必要がある。環境変数{0} と{1} が使用可能になり、{0} は{1} の相対変数になります。この機能と使用可能な環境変数の詳細については、{2} をお読みください。"
},
"sidebar": {
"help": "ヘルプ",
@@ -358,12 +376,21 @@
"hideNavButtons": "ナビゲーションボタンを隠す",
"hideNavButtonsDescription": "シェア内のナビバーにあるナビゲーションボタンを非表示にして、ミニマルな外観を作る。",
"viewModeDescription": "共有ページのデフォルトレイアウト。",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "シェアが送信されましたので、閲覧またはダウンロードしてください。",
"disableShareCard": "シェアカードを無効にする",
"disableShareCardDescription": "サイドバーの共有ページ、またはモバイルのページ上部にある共有カードを無効にする。",
"disableSidebar": "サイドバーを無効にする",
"disableSidebarDescription": "共有ページのサイドバーを無効にする。"
"disableSidebarDescription": "共有ページのサイドバーを無効にする。",
"enforceDarkLightMode": "テーマモードの強制",
"enforceDarkLightModeDescription": "この共有に特定のテーマモード(ダークまたはライト)を強制します。",
"default": "モードを強制しない",
"dark": "暗い",
"light": "ライト",
"enableOnlyOffice": "OnlyOfficeビューアを有効にする",
"enableOnlyOfficeDescription": "この共有でOnlyOfficeを使用したオフィスファイルの閲覧を許可する。",
"enableOnlyOfficeEditing": "OnlyOffice編集を有効にする",
"enableOnlyOfficeEditingDescription": "この共有でOnlyOfficeを使ったオフィスファイルの編集を許可する。",
"titleDefault": "共有ファイル{title}"
},
"api": {
"title": "APIキー:",
@@ -447,8 +474,8 @@
"totalDenied": "却下",
"totalAllowed": "可",
"all": "すべて拒否",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "これは、特定のアクセスルールが存在しないパスにユーザがアクセスしようとしたときに何が起こるかを示している。Allow」はユーザーがデフォルトでアクセスできることを意味し、「Deny」は明示的に許可されない限り、ユーザーがデフォルトでブロックされることを意味する。"
"defaultBehaviorDescription": "これは、特定のアクセスルールが存在しないパスにユーザがアクセスしようとしたときに何が起こるかを示している。Allow」はユーザーがデフォルトでアクセスできることを意味し、「Deny」は明示的に許可されない限り、ユーザーがデフォルトでブロックされることを意味する。",
"defaultBehavior": "デフォルト・アクション{suffix}"
},
"fileLoading": {
"title": "アップロードとダウンロード",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - デフォルトのテーマ",
"customTheme": "テーマを選ぶ",
"showSelectMultiple": "デスクトップのコンテキストメニューに複数選択を表示する",
"showSelectMultipleDescription": "デフォルトでは、デスクトップのコンテキストメニューには、モバイルのような「複数選択」オプションはありません。このオプションは常にコンテキストメニューに表示されます。"
"showSelectMultipleDescription": "デフォルトでは、デスクトップのコンテキストメニューには、モバイルのような「複数選択」オプションはありません。このオプションは常にコンテキストメニューに表示されます。",
"defaultMediaPlayer": "ブラウザのネイティブ・メディア・プレーヤーを使う",
"defaultMediaPlayerDescription": "ビデオやオーディオファイルには、FileBrowserに含まれる「vue-plyr」メディアプレーヤーではなく、ブラウザのネイティブメディアプレーヤーを使用してください。",
"debugOfficeEditor": "OnlyOfficeデバッグモードを有効にする",
"debugOfficeEditorDescription": "OnlyOfficeエディタに追加情報のデバッグポップアップを表示します。",
"fileViewerOptions": "ファイル・ビューアーのオプション",
"themeAndLanguage": "テーマと言語"
},
"editor": {
"uninitialized": "エディタの初期化に失敗しました。再読み込みしてください。",
"saveFailed": "ファイルの保存に失敗しました。もう一度お試しください。",
"saveAborted": "保存が中止された。ファイル名がアクティブなファイルと一致しません。",
"saveDisabled": "共有ファイルの保存はサポートされていません。",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "保存操作が中断されました。アプリケーションのアクティブファイル({activeFile} )と保存しようとしているファイル({tryingToSave} )が一致しません。",
"syncFailed": "{filename}\" のルートとステートの同期に失敗しました。データの破損を防ぐためにエディタのセットアップを中止しています。"
},
"player": {
"LoopEnabled": "ループ有効",
"LoopDisabled": "ループ無効"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "완료 지우기",
"showMore": "자세히 보기",
"showLess": "덜 보기",
"openParentFolder": "상위 폴더 열기"
"openParentFolder": "상위 폴더 열기",
"selectedCount": "작업을 수행하기 위해 선택한 항목 수"
},
"download": {
"downloadFile": "파일 다운로드",
@@ -84,15 +85,15 @@
"click": "파일이나 디렉토리를 선택해주세요.",
"ctrl": {
"click": "여러 개의 파일이나 디렉토리를 선택해주세요.",
"f": "검색창 열기",
"s": "파일 또는 디렉토리 다운로드"
"d": "선택한 항목 다운로드"
},
"del": "선택된 파일 삭제",
"doubleClick": "파일 또는 디렉토리 열기",
"esc": "선택 취소/프롬프트 닫기",
"f1": "정보",
"f2": "파일 이름 변경",
"help": "도움말"
"help": "도움말",
"f1": "도움말 프롬프트 표시",
"description": "아래에서 기본 탐색 옵션을 확인할 수 있습니다. 자세한 내용은 다음을 참조하세요.",
"wiki": "파일브라우저 퀀텀 위키"
},
"languages": {
"he": "עברית",
@@ -134,7 +135,6 @@
"prompts": {
"copy": "복사",
"copyMessage": "복사할 디렉토리:",
"deleteMessageMultiple": "{count} 개의 파일을 삭제하시겠습니까?",
"deleteMessageSingle": "파일 혹은 디렉토리를 삭제하시겠습니까?",
"deleteTitle": "파일 삭제",
"displayName": "게시 이름:",
@@ -142,7 +142,6 @@
"downloadMessage": "다운로드 포맷 설정.",
"error": "에러 발생!",
"fileInfo": "파일 정보",
"lastModified": "최종 수정{suffix}",
"move": "이동",
"moveMessage": "이동할 화일 또는 디렉토리를 선택하세요:",
"newArchetype": "원형을 유지하는 포스트를 생성합니다. 파일은 컨텐트 폴더에 생성됩니다.",
@@ -150,10 +149,7 @@
"newDirMessage": "새 디렉토리 이름을 입력해주세요.",
"newFile": "새 파일",
"newFileMessage": "새 파일 이름을 입력해주세요.",
"numberDirs": "디렉토리 수{suffix}",
"numberFiles": "파일 수{suffix}",
"rename": "이름 변경",
"renameMessage": "새로운 이름을 입력하세요.",
"replace": "대체하기",
"replaceMessage": "동일한 파일 이름이 존재합니다. 현재 파일을 덮어쓸까요?\n",
"schedule": "일정",
@@ -161,7 +157,6 @@
"show": "보기",
"size": "크기",
"upload": "업로드",
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
"deleteUserMessage": "이 사용자를 삭제하시겠습니까?",
"filesSelected": "선택한 파일",
"optionalPassword": "선택적 비밀번호",
@@ -172,10 +167,6 @@
"dragAndDrop": "파일을 여기로 끌어다 놓기",
"completed": "완료",
"conflictsDetected": "충돌 감지",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "제공된 이름이 잘못되었습니다: \"/\" 또는 \"\\\"를 포함할 수 없습니다.",
"source": "Source{suffix}",
"downloadsLimit": "다운로드 횟수 제한",
"maxBandwidth": "최대 다운로드 대역폭(kbps)",
"shareTheme": "테마 공유",
@@ -186,22 +177,44 @@
"shareBanner": "배너 URL 공유",
"shareFavicon": "파비콘 URL 공유",
"optionalPasswordHelp": "이 비밀번호를 설정하면 사용자가 공유 콘텐츠를 보려면 이 비밀번호를 입력해야 합니다.",
"viewMode": "보기 모드"
"viewMode": "보기 모드",
"renameMessage": "새 이름을 입력합니다:",
"renameMessageInvalid": "파일은 \"/\" 또는 \"\\\"를 포함할 수 없습니다.",
"source": "출처{suffix}",
"deleteMessageMultiple": "{count} 파일을 정말 삭제하시겠습니까?",
"deleteMessageShare": "이 공유를 삭제하시겠습니까? {path} ?",
"lastModified": "마지막 수정{suffix}",
"numberDirs": "디렉터리 수{suffix}",
"numberFiles": "파일 수{suffix}",
"renameMessageConflict": "\"{filename}\"라는 이름의 항목이 이미 존재합니다!",
"uploadSettingsChunked": "최대 동시 업로드: {maxConcurrentUpload}, 청크 크기: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "최대 동시 업로드: {maxConcurrentUpload}, 청크 업로드 비활성화"
},
"search": {
"images": "이미지",
"music": "음악",
"pdf": "PDF",
"pressToSearch": "검색하려면 엔터를 입력하세요",
"search": "검색...",
"typeToSearch": "검색어 입력...",
"types": "Types",
"video": "비디오",
"path": "경로:",
"smallerThan": "보다 작습니다:",
"largerThan": "보다 큽니다:",
"helpText1": "검색은 입력하는 각 문자(검색어의 경우 최소 3자)에 대해 이루어집니다.",
"helpText2": "색인: 검색은 설정된 간격(기본값: 5분)에 따라 자동으로 업데이트되는 색인을 활용합니다. 프로그램이 막 시작되었을 때 검색하면 불완전한 결과가 나올 수 있습니다."
"helpText2": "색인: 검색은 설정된 간격(기본값: 5분)에 따라 자동으로 업데이트되는 색인을 활용합니다. 프로그램이 막 시작되었을 때 검색하면 불완전한 결과가 나올 수 있습니다.",
"noResults": "색인 검색에서 결과를 찾을 수 없습니다.",
"onlyFolders": "폴더만",
"onlyFiles": "파일만",
"showOptions": "옵션 표시",
"photos": "사진",
"audio": "오디오",
"videos": "동영상",
"documents": "문서",
"archives": "아카이브",
"number": "번호",
"searchContext": "컨텍스트 검색: {context}",
"notEnoughCharacters": "검색할 문자가 부족합니다(최소 {minSearchLength})",
"typeToSearch": "검색을 시작하려면 {minSearchLength} 을 입력하세요."
},
"settings": {
"admin": "관리자",
@@ -214,10 +227,8 @@
"avoidChanges": "(수정하지 않으면 비워두세요)",
"branding": "브랜딩",
"brandingDirectoryPath": "브랜드 디렉토리 경로",
"brandingHelp": "File Browser 인스턴스는 이름, 로고, 스타일 등을 변경할 수 있습니다. 자세한 사항은 여기{0}에서 확인하세요.",
"changePassword": "비밀번호 변경",
"commandRunner": "명령 실행기",
"commandRunnerHelp": "이벤트에 해당하는 명령을 설정하세요. 줄당 1개의 명령을 적으세요. 환경 변수{0} 와 {1}이 사용가능하며, {0} 은 {1}에 상대 경로 입니다. 자세한 사항은 {2} 를 참조하세요.",
"commandsUpdated": "명령 수정됨!",
"createUserDir": "Auto create user home dir while adding new user",
"customStylesheet": "커스텀 스타일시트",
@@ -300,7 +311,14 @@
"adminOptions": "관리자 사용자 옵션",
"shareDuration": "만료",
"searchOptions": "검색 옵션",
"downloads": "다운로드"
"downloads": "다운로드",
"systemAdmin": "시스템 및 관리자",
"configViewer": "구성 뷰어",
"configViewerShowFull": "전체 구성 표시",
"configViewerShowComments": "댓글 표시",
"configViewerLoadConfig": "로드 구성",
"brandingHelp": "파일 브라우저 인스턴스의 이름을 변경하고, 로고를 바꾸고, 사용자 지정 스타일을 추가하고, GitHub에 대한 외부 링크를 비활성화하여 모양과 느낌을 사용자 지정할 수 있습니다.\n사용자 지정 브랜딩에 대한 자세한 내용은 {0} 을 참조하세요.",
"commandRunnerHelp": "여기에서 명명된 이벤트에서 실행되는 명령을 설정할 수 있습니다. 한 줄에 하나씩 작성해야 합니다. 환경 변수 {0} 및 {1} 을 사용할 수 있으며, {0} 은 {1} 에 상대적입니다. 이 기능 및 사용 가능한 환경 변수에 대한 자세한 내용은 {2} 을 참조하세요."
},
"sidebar": {
"help": "도움말",
@@ -358,12 +376,21 @@
"hideNavButtons": "탐색 버튼 숨기기",
"hideNavButtonsDescription": "공유의 탐색 모음에서 탐색 버튼을 숨겨 미니멀한 디자인을 만듭니다.",
"viewModeDescription": "공유 페이지의 기본 레이아웃입니다.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "보거나 다운로드할 수 있는 공유물이 전송되었습니다.",
"disableShareCard": "공유 카드 비활성화",
"disableShareCardDescription": "사이드바의 공유 페이지 또는 모바일의 페이지 상단에서 공유 카드를 비활성화합니다.",
"disableSidebar": "사이드바 비활성화",
"disableSidebarDescription": "공유 페이지에서 사이드바를 비활성화합니다."
"disableSidebarDescription": "공유 페이지에서 사이드바를 비활성화합니다.",
"enforceDarkLightMode": "테마 모드 적용",
"enforceDarkLightModeDescription": "이 공유에 특정 테마 모드(어둡거나 밝음)를 강제로 적용하여 사용자 기본 설정을 재정의합니다.",
"default": "모드 강제 적용 안 함",
"dark": "Dark",
"light": "빛",
"enableOnlyOffice": "Office 뷰어만 사용",
"enableOnlyOfficeDescription": "이 공유에서 OnlyOffice를 사용하여 Office 파일을 볼 수 있도록 허용합니다.",
"enableOnlyOfficeEditing": "Office 편집만 사용",
"enableOnlyOfficeEditingDescription": "이 공유에서 OnlyOffice를 사용하여 Office 파일을 편집할 수 있도록 허용합니다.",
"titleDefault": "공유 파일 - {title}"
},
"api": {
"title": "API 키:",
@@ -447,8 +474,8 @@
"totalDenied": "거부됨",
"totalAllowed": "허용됨",
"all": "모두 거부",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "사용자가 특정 액세스 규칙이 없는 경로에 액세스하려고 할 때 어떤 일이 발생하는지 보여줍니다. '허용'은 사용자가 기본적으로 액세스할 수 있음을 의미하며, '거부'는 명시적으로 허용되지 않는 한 기본적으로 차단됨을 의미합니다."
"defaultBehaviorDescription": "사용자가 특정 액세스 규칙이 없는 경로에 액세스하려고 할 때 어떤 일이 발생하는지 보여줍니다. '허용'은 사용자가 기본적으로 액세스할 수 있음을 의미하며, '거부'는 명시적으로 허용되지 않는 한 기본적으로 차단됨을 의미합니다.",
"defaultBehavior": "기본 작업{suffix}"
},
"fileLoading": {
"title": "업로드 및 다운로드",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - 기본 테마",
"customTheme": "테마 선택",
"showSelectMultiple": "데스크톱의 컨텍스트 메뉴에서 여러 개 선택 표시",
"showSelectMultipleDescription": "기본적으로 데스크톱 컨텍스트 메뉴에는 모바일처럼 '여러 개 선택' 옵션이 없습니다. 이 옵션은 항상 컨텍스트 메뉴에 표시됩니다."
"showSelectMultipleDescription": "기본적으로 데스크톱 컨텍스트 메뉴에는 모바일처럼 '여러 개 선택' 옵션이 없습니다. 이 옵션은 항상 컨텍스트 메뉴에 표시됩니다.",
"defaultMediaPlayer": "브라우저의 기본 미디어 플레이어 사용",
"defaultMediaPlayerDescription": "비디오 및 오디오 파일은 FileBrowser에 포함된 'vue-plyr' 미디어 플레이어가 아닌 브라우저의 기본 미디어 플레이어를 사용하세요.",
"debugOfficeEditor": "Office 디버그 모드만 사용",
"debugOfficeEditorDescription": "OnlyOffice 편집기에서 추가 정보가 포함된 디버그 팝업을 표시합니다.",
"fileViewerOptions": "파일 뷰어 옵션",
"themeAndLanguage": "테마 및 언어"
},
"editor": {
"uninitialized": "편집기를 초기화하지 못했습니다. 다시 로드하세요.",
"saveFailed": "파일을 저장하지 못했습니다. 다시 시도하세요.",
"saveAborted": "저장이 중단되었습니다. 파일 이름이 활성 파일과 일치하지 않습니다.",
"saveDisabled": "공유 파일 저장은 지원되지 않습니다.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "저장 작업이 중단되었습니다. 애플리케이션의 활성 파일({activeFile})이 저장하려는 파일({tryingToSave})과 일치하지 않습니다.",
"syncFailed": "5번 시도 후 \"{filename}\"의 경로와 상태를 동기화하지 못했습니다. 데이터 손상을 방지하기 위해 편집기 설정을 중단합니다."
},
"player": {
"LoopEnabled": "루프 사용",
"LoopDisabled": "루프 비활성화"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Gereed",
"showMore": "Meer tonen",
"showLess": "Minder tonen",
"openParentFolder": "Bovenliggende map openen"
"openParentFolder": "Bovenliggende map openen",
"selectedCount": "Aantal items geselecteerd om een actie uit te voeren"
},
"download": {
"downloadFile": "Bestand downloaden",
@@ -84,15 +85,15 @@
"click": "selecteer bestand of map",
"ctrl": {
"click": "selecteer meerdere bestanden of mappen",
"f": "opent zoeken",
"s": "sla een bestand op of download de map waar u bent"
"d": "geselecteerde items downloaden"
},
"del": "verwijder geselecteerde items",
"doubleClick": "open een bestand of map",
"esc": "Wis selectie en/of sluit de prompt",
"f1": "deze informatie",
"f2": "bestand herbenoemen",
"help": "Help"
"help": "Help",
"f1": "help prompt tonen",
"description": "Je kunt de basisnavigatieopties hieronder bekijken. Ga voor aanvullende informatie naar",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -134,7 +135,6 @@
"prompts": {
"copy": "Kopiëren",
"copyMessage": "Kies een locatie om uw bestanden te kopiëren: ",
"deleteMessageMultiple": "Weet u zeker dat u {count} bestand(en) wil verwijderen?",
"deleteMessageSingle": "Weet u zeker dat u dit bestand/map wil verwijderen?",
"deleteTitle": "Bestanden verwijderen",
"displayName": "Weergavenaam: ",
@@ -142,7 +142,6 @@
"downloadMessage": "Kies het vermat dat u wilt downloaden. ",
"error": "Er ging iets fout",
"fileInfo": "Bestandinformatie",
"lastModified": "Laatst Bewerkt{suffix}",
"move": "Verplaatsen",
"moveMessage": "Kies een nieuwe locatie voor je bestand(en)/map(pen)",
"newArchetype": "Maak een nieuw bericht op basis van een archetype. Uw bestand wordt aangemaakt in de inhoudsmap.",
@@ -150,10 +149,7 @@
"newDirMessage": "Schrijf de naam van de nieuwe map",
"newFile": "Nieuw bestand",
"newFileMessage": "Schrijf de naam van het nieuwe bestand",
"numberDirs": "Aantal mappen{suffix}",
"numberFiles": "Aantal bestanden{suffix}",
"rename": "Hernoemen",
"renameMessage": "Voer een nieuwe naam in voor",
"replace": "Vervangen",
"replaceMessage": "Een van de bestanden die u probeert te uploaden, geeft conflicten i.v.m. de naamgeving. Wilt u de bestaande bestanden vervangen?\n",
"schedule": "Plannen",
@@ -161,7 +157,6 @@
"show": "Tonen",
"size": "Grootte",
"upload": "Uploaden",
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
"deleteUserMessage": "Weet je zeker dat je deze gebruiker wilt verwijderen?",
"selected": "geselecteerd",
"filesSelected": "geselecteerde bestanden",
@@ -172,10 +167,6 @@
"dragAndDrop": "Sleep bestanden hier naartoe",
"completed": "Voltooid",
"conflictsDetected": "Gedetecteerde conflicten",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Ongeldige naam opgegeven: kan geen \"/\" of \"˜\" bevatten.",
"source": "Source{suffix}",
"downloadsLimit": "Limiet aantal downloads",
"maxBandwidth": "Max. downloadbandbreedte (kbps)",
"shareTheme": "Thema delen",
@@ -186,22 +177,44 @@
"shareBanner": "URL banner delen",
"shareFavicon": "favicon URL delen",
"optionalPasswordHelp": "Indien ingesteld, moeten gebruikers dit wachtwoord invoeren om de gedeelde inhoud te bekijken.",
"viewMode": "Weergavemodus"
"viewMode": "Weergavemodus",
"renameMessage": "Voer een nieuwe naam in:",
"renameMessageInvalid": "Een bestand kan geen \"/\" of \"\\\" bevatten.",
"source": "Bron{suffix}",
"deleteMessageMultiple": "Weet je zeker dat je {count} bestand(en) wilt verwijderen?",
"deleteMessageShare": "Weet je zeker dat je deze share wilt verwijderen: {path} ?",
"lastModified": "Laatst gewijzigd{suffix}",
"numberDirs": "Aantal mappen{suffix}",
"numberFiles": "Aantal bestanden{suffix}",
"renameMessageConflict": "Een item met de naam \"{filename}\" bestaat al!",
"uploadSettingsChunked": "Max gelijktijdige uploads: {maxConcurrentUpload}, brokgrootte: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max gelijktijdige uploads: {maxConcurrentUpload}, chunked upload uitgeschakeld"
},
"search": {
"images": "Afbeeldingen",
"music": "Muziek",
"pdf": "PDF",
"pressToSearch": "Druk op enter om te zoeken...",
"search": "Zoeken...",
"typeToSearch": "Typ om te zoeken...",
"types": "Types",
"video": "Video",
"path": "Pad:",
"smallerThan": "Kleiner dan:",
"largerThan": "Groter dan:",
"helpText1": "Er wordt gezocht op elk teken dat je typt (minimaal 3 tekens voor zoektermen).",
"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."
"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.",
"noResults": "Geen resultaten gevonden in geïndexeerde zoekopdrachten.",
"onlyFolders": "Alleen mappen",
"onlyFiles": "Alleen bestanden",
"showOptions": "Opties weergeven",
"photos": "Foto's",
"audio": "Audio",
"videos": "Video's",
"documents": "Documenten",
"archives": "Archief",
"number": "Aantal",
"searchContext": "Zoek context: {context}",
"notEnoughCharacters": "Niet genoeg tekens om te zoeken (min {minSearchLength})",
"typeToSearch": "Begin met het typen van {minSearchLength} of meer tekens om te beginnen met zoeken."
},
"settings": {
"admin": "Admin",
@@ -214,10 +227,8 @@
"avoidChanges": "(laat leeg om wijzigingen te voorkomen)",
"branding": "Branding",
"brandingDirectoryPath": "Branding directory path",
"brandingHelp": "U kunt de looks en feels hoe File Browser wordt getoond wijzigen door de naam aan te passen, het logo te vervangen, een aangepast stylesheet toe te voegen en/of de externe links naar GitHub uit te schakelen.\nVoor meer informatie over custom branding, bekijk {0}.",
"changePassword": "Wijzig Wachtwoord",
"commandRunner": "Command runner",
"commandRunnerHelp": "Hier kunt u opdrachten instellen die worden uitgevoerd in de benoemde gebeurtenissen. U moet er één per regel schrijven. De omgevingsvariabelen {0} en {1} zijn beschikbaar, zijnde {0} relatief ten opzichte van {1}. Raadpleeg {2} voor meer informatie over deze functie en de beschikbare omgevingsvariabelen.",
"commandsUpdated": "Commando's bijgewerkt!",
"createUserDir": "Maak automatisch een thuismap aan wanneer een nieuwe gebruiker wordt aangemaakt",
"customStylesheet": "Aangepast Stylesheet",
@@ -300,7 +311,14 @@
"adminOptions": "Opties voor beheerders",
"shareDuration": "Verloopt op",
"searchOptions": "Zoekopties",
"downloads": "Downloads"
"downloads": "Downloads",
"systemAdmin": "Systeem & Beheer",
"configViewer": "Configuratiescherm",
"configViewerShowFull": "Volledige configuratie weergeven",
"configViewerShowComments": "Opmerkingen tonen",
"configViewerLoadConfig": "Configuratie laden",
"brandingHelp": "Je kunt aanpassen hoe je File Browser instance eruit ziet en aanvoelt door de naam te veranderen, het logo te vervangen, aangepaste stijlen toe te voegen en zelfs externe links naar GitHub uit te schakelen.\nVoor meer informatie over het aanpassen van je branding, kun je kijken op {0}.",
"commandRunnerHelp": "Hier kun je commando's instellen die worden uitgevoerd in de genoemde gebeurtenissen. Je moet er één per regel schrijven. De omgevingsvariabelen {0} en {1} zullen beschikbaar zijn, waarbij {0} relatief is ten opzichte van {1}. Lees voor meer informatie over deze functie en de beschikbare omgevingsvariabelen de {2}."
},
"sidebar": {
"help": "Help",
@@ -358,12 +376,21 @@
"hideNavButtons": "Navigatieknoppen verbergen",
"hideNavButtonsDescription": "Verberg de navigatieknoppen op de navigatiebalk in de share om een minimalistische look te creëren.",
"viewModeDescription": "Standaard lay-out voor de gedeelde pagina.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Er is een share naar je verzonden om te bekijken of te downloaden.",
"disableShareCard": "Aandelenkaart uitschakelen",
"disableShareCardDescription": "Schakel de deelkaart uit op de gedeelde pagina in de zijbalk of bovenaan de pagina op mobiel.",
"disableSidebar": "Zijbalk uitschakelen",
"disableSidebarDescription": "Schakel de zijbalk op de gedeelde pagina uit."
"disableSidebarDescription": "Schakel de zijbalk op de gedeelde pagina uit.",
"enforceDarkLightMode": "Themamodus afdwingen",
"enforceDarkLightModeDescription": "Forceer een specifieke themamodus (donker of licht) voor deze share, waarbij gebruikersvoorkeuren worden overschreven.",
"default": "Een modus niet afdwingen",
"dark": "Donker",
"light": "Licht",
"enableOnlyOffice": "OnlyOffice viewer inschakelen",
"enableOnlyOfficeDescription": "Sta het bekijken van Office-bestanden toe met OnlyOffice in deze share.",
"enableOnlyOfficeEditing": "Bewerken met OnlyOffice inschakelen",
"enableOnlyOfficeEditingDescription": "Laat het bewerken van Office-bestanden met OnlyOffice toe in deze share.",
"titleDefault": "Gedeelde bestanden - {title}"
},
"api": {
"title": "API-sleutels",
@@ -478,7 +505,13 @@
"defaultThemeDescription": "standaard - Het standaardthema",
"customTheme": "Kies je thema",
"showSelectMultiple": "Meerdere selecteren tonen in contextmenu op bureaublad",
"showSelectMultipleDescription": "Standaard heeft het contextmenu op de desktop geen optie \"meerdere selecteren\" zoals mobiel. Deze optie wordt altijd weergegeven in het contextmenu."
"showSelectMultipleDescription": "Standaard heeft het contextmenu op de desktop geen optie \"meerdere selecteren\" zoals mobiel. Deze optie wordt altijd weergegeven in het contextmenu.",
"defaultMediaPlayer": "Gebruik de ingebouwde mediaspeler van uw browser",
"defaultMediaPlayerDescription": "Gebruik de eigen mediaspeler van uw browser voor video- en audiobestanden in plaats van de meegeleverde 'vue-plyr' mediaspeler van FileBrowser.",
"debugOfficeEditor": "Debugmodus OnlyOffice inschakelen",
"debugOfficeEditorDescription": "Een foutopsporingspop-up weergeven met aanvullende informatie in de OnlyOffice-editor.",
"fileViewerOptions": "Opties voor bestandsviewer",
"themeAndLanguage": "Thema & Taal"
},
"general": {
"name": "Naam",
@@ -513,15 +546,19 @@
"name": "Naam",
"accessManagement": "Toegangsbeheer",
"all": "Alles weigeren",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Dit laat zien wat er gebeurt als een gebruiker toegang probeert te krijgen tot een pad waarvoor geen specifieke toegangsregels bestaan. Toestaan' betekent dat gebruikers standaard toegang hebben, 'Weigeren' betekent dat gebruikers standaard worden geblokkeerd tenzij ze expliciet toestemming krijgen."
"defaultBehaviorDescription": "Dit laat zien wat er gebeurt als een gebruiker toegang probeert te krijgen tot een pad waarvoor geen specifieke toegangsregels bestaan. Toestaan' betekent dat gebruikers standaard toegang hebben, 'Weigeren' betekent dat gebruikers standaard worden geblokkeerd tenzij ze expliciet toestemming krijgen.",
"defaultBehavior": "Standaardactie{suffix}"
},
"editor": {
"uninitialized": "Het is niet gelukt de editor te initialiseren. Gelieve opnieuw te laden.",
"saveFailed": "Het is niet gelukt om het bestand op te slaan. Probeer het opnieuw.",
"saveAborted": "Opslaan afgebroken. De bestandsnaam komt niet overeen met het actieve bestand.",
"saveDisabled": "Het opslaan van gedeelde bestanden wordt niet ondersteund.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Opslaan afgebroken. Het actieve bestand van de toepassing ({activeFile}) komt niet overeen met het bestand dat u probeert op te slaan ({tryingToSave}).",
"syncFailed": "Na 5 pogingen is het niet gelukt om de status te synchroniseren met de route voor \"{filename}\". Installatie van editor afgebroken om gegevensbeschadiging te voorkomen."
},
"player": {
"LoopEnabled": "Lus ingeschakeld",
"LoopDisabled": "Lus uitgeschakeld"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Czyszczenie zakończone",
"showMore": "Pokaż więcej",
"showLess": "Pokaż mniej",
"openParentFolder": "Otwórz folder nadrzędny"
"openParentFolder": "Otwórz folder nadrzędny",
"selectedCount": "Liczba elementów wybranych do wykonania akcji"
},
"download": {
"downloadFile": "Pobierz plik",
@@ -84,15 +85,15 @@
"click": "wybierz plik lub foler",
"ctrl": {
"click": "wybierz wiele plików lub folderów",
"f": "otwórz wyszukiwarkę",
"s": "pobierz aktywny plik lub folder"
"d": "Pobierz wybrane elementy"
},
"del": "usuń zaznaczone",
"doubleClick": "otwórz plik lub folder",
"esc": "wyczyść zaznaczenie i/lub zamknij okno z powiadomieniem",
"f1": "ta informacja",
"f2": "zmień nazwę pliku",
"help": "Pomoc"
"help": "Pomoc",
"f1": "pokaż monit pomocy",
"description": "Poniżej znajdują się podstawowe opcje nawigacji. Dodatkowe informacje można znaleźć na stronie",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -134,7 +135,6 @@
"prompts": {
"copy": "Kopiuj",
"copyMessage": "Wybierz lokalizację do której mają być skopiowane wybrane pliki",
"deleteMessageMultiple": "Czy jesteś pewien że chcesz usunąć {count} plik(ów)?",
"deleteMessageSingle": "Czy jesteś pewien, że chcesz usunąć ten plik/folder?",
"deleteTitle": "Usuń pliki",
"displayName": "Wyświetlana Nazwa:",
@@ -142,7 +142,6 @@
"downloadMessage": "Wybierz format, jaki chesz pobrać.",
"error": "Pojawił się nieznany błąd",
"fileInfo": "Informacje o pliku",
"lastModified": "Ostatnio Zmodyfikowane{suffix}",
"move": "Przenieś",
"moveMessage": "Wybierz nową lokalizację dla swoich plik(ów)/folder(ów):",
"newArchetype": "Utwórz nowy wpis na bazie wybranego wzorca. Twój plik będzie utworzony w wybranym folderze.",
@@ -150,10 +149,7 @@
"newDirMessage": "Podaj nazwę tworzonego folderu.",
"newFile": "Nowy plik",
"newFileMessage": "Podaj nazwętworzonego pliku.",
"numberDirs": "Ilość katalogów{suffix}",
"numberFiles": "Ilość plików{suffix}",
"rename": "Zmień nazwę",
"renameMessage": "Podaj nową nazwę dla",
"replace": "Zamień",
"replaceMessage": "Jednen z plików który próbujesz wrzucić próbje nadpisać plik o tej samej nazwie. Czy chcesz nadpisać poprzedni plik?\n",
"schedule": "Grafi",
@@ -161,7 +157,6 @@
"show": "Pokaż",
"size": "Rozmiar",
"upload": "Prześlij",
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
"deleteUserMessage": "Czy na pewno chcesz usunąć tego użytkownika?",
"filesSelected": "wybrane pliki",
"optionalPassword": "Hasło opcjonalne",
@@ -172,10 +167,6 @@
"dragAndDrop": "Przeciągnij i upuść pliki tutaj",
"completed": "Zakończono",
"conflictsDetected": "Wykryte konflikty",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Nieprawidłowa nazwa: nie można użyć kontenera \"/\" lub \"\\\"",
"source": "Source{suffix}",
"downloadsLimit": "Limit liczby pobrań",
"maxBandwidth": "Maksymalna przepustowość pobierania (kbps)",
"shareTheme": "Udostępnij temat",
@@ -186,22 +177,44 @@
"shareBanner": "Udostępnij adres URL banera",
"shareFavicon": "Udostępnij adres URL favicon",
"optionalPasswordHelp": "Jeśli jest ustawione, użytkownicy muszą wprowadzić to hasło, aby wyświetlić udostępnioną zawartość.",
"viewMode": "Tryb widoku"
"viewMode": "Tryb widoku",
"renameMessage": "Wprowadź nową nazwę:",
"renameMessageInvalid": "Plik nie może być kontenerem \"/\" lub \"\\\"",
"source": "Źródło{suffix}",
"deleteMessageMultiple": "Czy na pewno chcesz usunąć plik(i) {count}?",
"deleteMessageShare": "Czy na pewno chcesz usunąć ten udział: {path} ?",
"lastModified": "Ostatnia modyfikacja{suffix}",
"numberDirs": "Liczba katalogów{suffix}",
"numberFiles": "Liczba plików{suffix}",
"renameMessageConflict": "Element o nazwie \"{filename}\" już istnieje!",
"uploadSettingsChunked": "Maks. jednoczesne przesyłanie: {maxConcurrentUpload}, rozmiar fragmentu: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Maksymalny jednoczesny upload: {maxConcurrentUpload}, przesyłanie fragmentaryczne wyłączone"
},
"search": {
"images": "Zdjęcia",
"music": "Muzyka",
"pdf": "PDF",
"pressToSearch": "Wciśnij enter, aby wyszukać...",
"search": "Szukaj...",
"typeToSearch": "Zacznij pisać, aby wyszukać...",
"types": "Typy",
"video": "Wideo",
"path": "Ścieżka:",
"smallerThan": "Mniejszy niż:",
"largerThan": "Większy niż:",
"helpText1": "Wyszukiwanie odbywa się po każdym wpisanym znaku (minimum 3 znaki dla wyszukiwanych haseł).",
"helpText2": "Indeks: Wyszukiwanie wykorzystuje indeks, który jest automatycznie aktualizowany w skonfigurowanych odstępach czasu (domyślnie: 5 minut). Wyszukiwanie po uruchomieniu programu może skutkować niekompletnymi wynikami."
"helpText2": "Indeks: Wyszukiwanie wykorzystuje indeks, który jest automatycznie aktualizowany w skonfigurowanych odstępach czasu (domyślnie: 5 minut). Wyszukiwanie po uruchomieniu programu może skutkować niekompletnymi wynikami.",
"noResults": "Nie znaleziono żadnych wyników w indeksowanym wyszukiwaniu.",
"onlyFolders": "Tylko foldery",
"onlyFiles": "Tylko pliki",
"showOptions": "Pokaż opcje",
"photos": "Zdjęcia",
"audio": "Audio",
"videos": "Filmy",
"documents": "Dokumenty",
"archives": "Archiwa",
"number": "Liczba",
"searchContext": "Kontekst wyszukiwania: {context}",
"notEnoughCharacters": "Za mało znaków do wyszukania (min. {minSearchLength})",
"typeToSearch": "Zacznij wpisywać {minSearchLength} lub więcej znaków, aby rozpocząć wyszukiwanie."
},
"settings": {
"admin": "Admin",
@@ -214,10 +227,8 @@
"avoidChanges": "(pozostaw puste aby nie zosatało zmienione)",
"branding": "Branding",
"brandingDirectoryPath": "Folder brandingowy",
"brandingHelp": "Możesz dostosować wygląd i doznania użytkownika swojej instancji File Browser poprzez zmianę jej nazwy, zmianę logo, dodanie własnych stylów, a nawet wyłączyć linki zewnętrzne do GitHuba.\nW celu pozyskania większej ilości informacji nt. osobistego brandingu, zapoznaj się z {0}.",
"changePassword": "Zmień Hasło",
"commandRunner": "Narzędzie do wykonywania poleceń",
"commandRunnerHelp": "Tu możesz ustawić komendy, które będą wykonywane przy danych zdarzeniach. Musisz wpisywać po jednej na linjkę. Zmienne środowiskowe {0} i {1} będą dostępne, gdzie {0} jest względne wobec {1}. Więcej informacji o tej funkcji i dostępnych zmiennych środowiskowych znajdziesz tutaj: {2}.",
"commandsUpdated": "Polecenie zaktualizowane!",
"createUserDir": "Automatycznie utwórz katalog domowy użytkownika podczas dodania nowego użytkownika",
"customStylesheet": "Własny arkusz stylów",
@@ -300,7 +311,14 @@
"adminOptions": "Opcje użytkownika administratora",
"shareDuration": "Wygasa",
"searchOptions": "Opcje wyszukiwania",
"downloads": "Pliki do pobrania"
"downloads": "Pliki do pobrania",
"systemAdmin": "System i administrator",
"configViewer": "Przeglądarka konfiguracji",
"configViewerShowFull": "Pokaż pełną konfigurację",
"configViewerShowComments": "Pokaż komentarze",
"configViewerLoadConfig": "Konfiguracja ładowania",
"brandingHelp": "Możesz dostosować wygląd i sposób działania swojej instancji przeglądarki plików, zmieniając jej nazwę, zastępując logo, dodając niestandardowe style, a nawet wyłączając zewnętrzne łącza do GitHub.\nWięcej informacji na temat niestandardowego brandingu można znaleźć na stronie {0}.",
"commandRunnerHelp": "Tutaj można ustawić polecenia, które będą wykonywane w nazwanych zdarzeniach. Należy wpisać jedną komendę w wierszu. Dostępne będą zmienne środowiskowe {0} i {1}, przy czym {0} będzie relatywna do {1}. Więcej informacji na temat tej funkcji i dostępnych zmiennych środowiskowych można znaleźć na stronie {2}."
},
"sidebar": {
"help": "Pomoc",
@@ -358,12 +376,21 @@
"hideNavButtons": "Ukryj przyciski nawigacyjne",
"hideNavButtonsDescription": "Ukryj przyciski nawigacyjne na pasku nawigacyjnym w folderze udostępnionym, aby uzyskać minimalistyczny wygląd.",
"viewModeDescription": "Domyślny układ strony udostępnionej.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Udział został wysłany do wyświetlenia lub pobrania.",
"disableShareCard": "Wyłączanie karty udostępniania",
"disableShareCardDescription": "Wyłącz kartę udostępniania na udostępnianej stronie na pasku bocznym lub u góry strony na urządzeniach mobilnych.",
"disableSidebar": "Wyłącz pasek boczny",
"disableSidebarDescription": "Wyłącz pasek boczny na udostępnionej stronie."
"disableSidebarDescription": "Wyłącz pasek boczny na udostępnionej stronie.",
"enforceDarkLightMode": "Wymuszanie trybu motywu",
"enforceDarkLightModeDescription": "Wymusza określony tryb motywu (ciemny lub jasny) dla tego udziału, zastępując preferencje użytkownika.",
"default": "Nie wymuszaj trybu",
"dark": "Ciemny",
"light": "Światło",
"enableOnlyOffice": "Włącz przeglądarkę OnlyOffice",
"enableOnlyOfficeDescription": "Zezwalaj na wyświetlanie plików biurowych przy użyciu OnlyOffice w tym udziale.",
"enableOnlyOfficeEditing": "Włącz edycję OnlyOffice",
"enableOnlyOfficeEditingDescription": "Zezwalaj na edycję plików biurowych za pomocą OnlyOffice w tym udziale.",
"titleDefault": "Udostępnione pliki - {title}"
},
"api": {
"title": "Klucze API:",
@@ -447,8 +474,8 @@
"totalDenied": "Odmowa",
"totalAllowed": "Dozwolone",
"all": "Odmów wszystkim",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "To pokazuje, co się dzieje, gdy użytkownik próbuje uzyskać dostęp do ścieżki, dla której nie istnieją określone reguły dostępu. \"Zezwól\" oznacza, że użytkownicy mogą uzyskać dostęp domyślnie, \"Odmów\" oznacza, że użytkownicy są domyślnie blokowani, chyba że wyraźnie na to zezwolono."
"defaultBehaviorDescription": "To pokazuje, co się dzieje, gdy użytkownik próbuje uzyskać dostęp do ścieżki, dla której nie istnieją określone reguły dostępu. \"Zezwól\" oznacza, że użytkownicy mogą uzyskać dostęp domyślnie, \"Odmów\" oznacza, że użytkownicy są domyślnie blokowani, chyba że wyraźnie na to zezwolono.",
"defaultBehavior": "Działanie domyślne{suffix}"
},
"fileLoading": {
"title": "Przesyłanie i pobieranie",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - domyślny motyw",
"customTheme": "Wybierz motyw",
"showSelectMultiple": "Wyświetlanie wyboru wielokrotnego w menu kontekstowym na pulpicie",
"showSelectMultipleDescription": "Domyślnie menu kontekstowe pulpitu nie ma opcji \"wybierz wiele\", tak jak w przypadku urządzeń mobilnych. Opcja ta jest zawsze widoczna w menu kontekstowym."
"showSelectMultipleDescription": "Domyślnie menu kontekstowe pulpitu nie ma opcji \"wybierz wiele\", tak jak w przypadku urządzeń mobilnych. Opcja ta jest zawsze widoczna w menu kontekstowym.",
"defaultMediaPlayer": "Użyj natywnego odtwarzacza multimediów w przeglądarce",
"defaultMediaPlayerDescription": "Do odtwarzania plików wideo i audio należy używać natywnego odtwarzacza multimediów przeglądarki, a nie dołączonego do FileBrowser odtwarzacza multimediów \"vue-plyr\".",
"debugOfficeEditor": "Włącz tryb debugowania OnlyOffice",
"debugOfficeEditorDescription": "Wyświetla wyskakujące okienko debugowania z dodatkowymi informacjami w edytorze OnlyOffice.",
"fileViewerOptions": "Opcje przeglądarki plików",
"themeAndLanguage": "Temat i język"
},
"editor": {
"uninitialized": "Nie udało się zainicjować edytora. Przeładuj ponownie.",
"saveFailed": "Nie udało się zapisać pliku. Spróbuj ponownie.",
"saveAborted": "Zapisywanie przerwane. Nazwa pliku nie pasuje do aktywnego pliku.",
"saveDisabled": "Zapisywanie udostępnionych plików nie jest obsługiwane.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Operacja zapisywania została przerwana. Aktywny plik aplikacji ({activeFile}) nie jest zgodny z plikiem, który próbujesz zapisać ({tryingToSave}).",
"syncFailed": "Nie udało się zsynchronizować stanu z trasą dla \"{filename}\" po 5 próbach. Przerwano konfigurację edytora, aby zapobiec uszkodzeniu danych."
},
"player": {
"LoopEnabled": "Włączona pętla",
"LoopDisabled": "Pętla wyłączona"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Limpar concluído",
"showMore": "Mostrar mais",
"showLess": "Mostrar menos",
"openParentFolder": "Abrir a pasta principal"
"openParentFolder": "Abrir a pasta principal",
"selectedCount": "Número de itens selecionados para executar uma ação"
},
"download": {
"downloadFile": "Baixar arquivo",
@@ -84,15 +85,15 @@
"click": "selecionar pasta ou arquivo",
"ctrl": {
"click": "selecionar várias pastas e arquivos",
"f": "pesquisar",
"s": "salvar um arquivo ou baixar a pasta que você está"
"d": "baixar itens selecionados"
},
"del": "apagar os arquivos selecionados",
"doubleClick": "abrir pasta ou arquivo",
"esc": "limpar seleção e/ou fechar menu",
"f1": "esta informação",
"f2": "renomear arquivo",
"help": "Ajuda"
"help": "Ajuda",
"f1": "mostrar prompt de ajuda",
"description": "Você pode ver as opções básicas de navegação abaixo. Para obter informações adicionais, visite",
"wiki": "Wiki do FileBrowser Quantum"
},
"languages": {
"he": "עברית",
@@ -134,16 +135,13 @@
"prompts": {
"copy": "Copiar",
"copyMessage": "Escolha um lugar para copiar os arquivos:",
"deleteMessageMultiple": "Deseja apagar {count} arquivo(s)?",
"deleteMessageSingle": "Deseja apagar esta pasta/arquivo?",
"deleteMessageShare": "Deseja apagar este compartilhamento ({path})?",
"deleteTitle": "Apagar arquivos",
"displayName": "Nome:",
"download": "Baixar arquivos",
"downloadMessage": "Escolha o formato do arquivo.",
"error": "Algo de errado ocorreu",
"fileInfo": "Informação do arquivo",
"lastModified": "Última modificação{suffix}",
"move": "Mover",
"moveMessage": "Escolha uma nova pasta para os seus arquivos:",
"newArchetype": "Criar um novo post baseado num \"archetype\". O seu arquivo será criado na pasta \"content\".",
@@ -151,10 +149,7 @@
"newDirMessage": "Escreva o nome da nova pasta.",
"newFile": "Novo arquivo",
"newFileMessage": "Escreva o nome do novo arquivo.",
"numberDirs": "Número de pastas{suffix}",
"numberFiles": "Número de arquivos{suffix}",
"rename": "Renomear",
"renameMessage": "Insira um novo nome para",
"replace": "Substituir",
"replaceMessage": "Já existe um arquivo com nome igual a um dos que está tentando enviar. Deseja substituir?\n",
"schedule": "Agendar",
@@ -172,10 +167,6 @@
"dragAndDrop": "Arraste e solte os arquivos aqui",
"completed": "Concluído",
"conflictsDetected": "Conflitos detectados",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Nome inválido fornecido: não pode conter \"/\" ou \"\\\"",
"source": "Source{suffix}",
"downloadsLimit": "Limite de número de downloads",
"maxBandwidth": "Largura de banda máxima de download (kbps)",
"shareTheme": "Compartilhar tema",
@@ -186,22 +177,44 @@
"shareBanner": "Compartilhar URL do banner",
"shareFavicon": "Compartilhar URL do favicon",
"optionalPasswordHelp": "Se definida, os usuários deverão digitar essa senha para visualizar o conteúdo compartilhado.",
"viewMode": "Modo de exibição"
"viewMode": "Modo de exibição",
"renameMessage": "Digite um novo nome:",
"renameMessageInvalid": "Um arquivo não pode ter o contêiner \"/\" ou \"\\\"",
"source": "Fonte{suffix}",
"deleteMessageMultiple": "Tem certeza de que deseja excluir o(s) arquivo(s) {count}?",
"deleteMessageShare": "Tem certeza de que deseja excluir esse compartilhamento: {path} ?",
"lastModified": "Última modificação{suffix}",
"numberDirs": "Número de diretórios{suffix}",
"numberFiles": "Número de arquivos{suffix}",
"renameMessageConflict": "Já existe um item chamado \"{filename}\"!",
"uploadSettingsChunked": "Máximo de uploads simultâneos: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Máximo de uploads simultâneos: {maxConcurrentUpload}, upload em pedaços desativado"
},
"search": {
"images": "Imagens",
"music": "Músicas",
"pdf": "PDF",
"pressToSearch": "Pressione Enter para pesquisar...",
"search": "Pesquise...",
"typeToSearch": "Digite para pesquisar...",
"types": "Tipos",
"video": "Vídeos",
"path": "Caminho:",
"smallerThan": "Menor que:",
"largerThan": "Maior que:",
"helpText1": "A pesquisa ocorre em cada caractere que você digita (mínimo de 3 caracteres para termos de pesquisa).",
"helpText2": "O índice: A pesquisa utiliza o índice que é atualizado automaticamente no intervalo configurado (padrão: 5 minutos). A pesquisa quando o programa acaba de ser iniciado pode resultar em resultados incompletos."
"helpText2": "O índice: A pesquisa utiliza o índice que é atualizado automaticamente no intervalo configurado (padrão: 5 minutos). A pesquisa quando o programa acaba de ser iniciado pode resultar em resultados incompletos.",
"noResults": "Não foram encontrados resultados na pesquisa indexada.",
"onlyFolders": "Somente pastas",
"onlyFiles": "Somente arquivos",
"showOptions": "Mostrar opções",
"photos": "Fotos",
"audio": "Áudio",
"videos": "Vídeos",
"documents": "Documentos",
"archives": "Arquivos",
"number": "Número",
"searchContext": "Contexto da pesquisa: {context}",
"notEnoughCharacters": "Não há caracteres suficientes para pesquisar (mín. {minSearchLength})",
"typeToSearch": "Comece a digitar {minSearchLength} ou mais caracteres para iniciar a busca."
},
"settings": {
"admin": "Admin",
@@ -214,10 +227,8 @@
"avoidChanges": "(deixe em branco para manter)",
"branding": "Customização",
"brandingDirectoryPath": "Diretório de customização",
"brandingHelp": "Você pode mudar a aparência e experiência de sua instância do File Browser alterando seu nome, logotipo, adicionando estilos customizados e até desabilitando links externos para o GitHub.\nPara mais informações sobre customizações, confira {0}.",
"changePassword": "Alterar senha",
"commandRunner": "Execução de comandos",
"commandRunnerHelp": "Aqui você pode definir comandos que serão executados nos eventos descritos. Escreva um por linha. As variáveis de ambiente {0} e {1} estão disponíveis, sendo {0} relativo a {1}. Para mais informações sobre esta função e as variáveis de ambiente disponíveis, leia a {2}.",
"commandsUpdated": "Comandos atualizados!",
"createUserDir": "Criar diretório Home para novos usuários",
"userHomeBasePath": "Caminho base para diretórios de usuários",
@@ -300,7 +311,14 @@
"adminOptions": "Opções do usuário administrador",
"shareDuration": "Expirações",
"searchOptions": "Opções de pesquisa",
"downloads": "Downloads"
"downloads": "Downloads",
"systemAdmin": "Sistema e administração",
"configViewer": "Visualizador de configuração",
"configViewerShowFull": "Mostrar configuração completa",
"configViewerShowComments": "Mostrar comentários",
"configViewerLoadConfig": "Configuração de carga",
"brandingHelp": "Você pode personalizar a aparência da instância do Navegador de arquivos alterando seu nome, substituindo o logotipo, adicionando estilos personalizados e até mesmo desativando links externos para o GitHub.\nPara obter mais informações sobre branding personalizado, consulte {0}.",
"commandRunnerHelp": "Aqui você pode definir comandos que são executados nos eventos nomeados. Você deve escrever um por linha. As variáveis de ambiente {0} e {1} estarão disponíveis, sendo {0} relativo a {1}. Para obter mais informações sobre esse recurso e as variáveis de ambiente disponíveis, leia {2}."
},
"sidebar": {
"help": "Ajuda",
@@ -358,12 +376,21 @@
"hideNavButtons": "Ocultar botões de navegação",
"hideNavButtonsDescription": "Oculte os botões de navegação na barra de navegação do compartilhamento para criar uma aparência minimalista.",
"viewModeDescription": "Layout padrão para a página compartilhada.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Um compartilhamento foi enviado a você para visualização ou download.",
"disableShareCard": "Desativar cartão compartilhado",
"disableShareCardDescription": "Desative o cartão de compartilhamento na página compartilhada na barra lateral ou na parte superior da página no celular.",
"disableSidebar": "Desativar a barra lateral",
"disableSidebarDescription": "Desative a barra lateral na página compartilhada."
"disableSidebarDescription": "Desative a barra lateral na página compartilhada.",
"enforceDarkLightMode": "Aplicar o modo de tema",
"enforceDarkLightModeDescription": "Força um modo de tema específico (escuro ou claro) para esse compartilhamento, substituindo as preferências do usuário.",
"default": "Não imponha um modo",
"dark": "Escuro",
"light": "Luz",
"enableOnlyOffice": "Ativar o visualizador do OnlyOffice",
"enableOnlyOfficeDescription": "Permitir a visualização de arquivos do Office usando o OnlyOffice nesse compartilhamento.",
"enableOnlyOfficeEditing": "Ativar a edição do OnlyOffice",
"enableOnlyOfficeEditingDescription": "Permitir a edição de arquivos do Office usando o OnlyOffice nesse compartilhamento.",
"titleDefault": "Arquivos compartilhados - {title}"
},
"api": {
"title": "Chaves de API:",
@@ -447,8 +474,8 @@
"totalDenied": "Negado",
"totalAllowed": "Permitido",
"all": "Negar tudo",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Isso mostra o que acontece quando um usuário tenta acessar um caminho em que não existem regras de acesso específicas. \"Permitir\" significa que os usuários podem acessar por padrão, \"Negar\" significa que os usuários são bloqueados por padrão, a menos que sejam explicitamente permitidos."
"defaultBehaviorDescription": "Isso mostra o que acontece quando um usuário tenta acessar um caminho em que não existem regras de acesso específicas. \"Permitir\" significa que os usuários podem acessar por padrão, \"Negar\" significa que os usuários são bloqueados por padrão, a menos que sejam explicitamente permitidos.",
"defaultBehavior": "Ação padrão{suffix}"
},
"fileLoading": {
"title": "Uploads e downloads",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - O tema padrão",
"customTheme": "Escolha seu tema",
"showSelectMultiple": "Mostrar seleção múltipla no menu de contexto na área de trabalho",
"showSelectMultipleDescription": "Por padrão, o menu de contexto do desktop não tem a opção \"selecionar vários\" como no celular. Essa opção sempre a mostra no menu de contexto."
"showSelectMultipleDescription": "Por padrão, o menu de contexto do desktop não tem a opção \"selecionar vários\" como no celular. Essa opção sempre a mostra no menu de contexto.",
"defaultMediaPlayer": "Use o reprodutor de mídia nativo do seu navegador",
"defaultMediaPlayerDescription": "Use o reprodutor de mídia nativo do seu navegador para arquivos de vídeo e áudio em vez do reprodutor de mídia 'vue-plyr' incluído no FileBrowser.",
"debugOfficeEditor": "Ativar o modo de depuração do OnlyOffice",
"debugOfficeEditorDescription": "Mostrar um pop-up de depuração com informações adicionais no editor do OnlyOffice.",
"fileViewerOptions": "Opções do visualizador de arquivos",
"themeAndLanguage": "Tema e linguagem"
},
"editor": {
"uninitialized": "Falha ao inicializar o editor. Favor recarregar.",
"saveFailed": "Falha ao salvar o arquivo. Tente novamente.",
"saveAborted": "Salvamento abortado. O nome do arquivo não corresponde ao arquivo ativo.",
"saveDisabled": "Não há suporte para o salvamento de arquivos compartilhados.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Operação de salvamento abortada. O arquivo ativo do aplicativo ({activeFile}) não corresponde ao arquivo que você está tentando salvar ({tryingToSave}).",
"syncFailed": "Falha ao sincronizar o estado com a rota para \"{filename}\" após 5 tentativas. Abortar a configuração do editor para evitar a corrupção de dados."
},
"player": {
"LoopEnabled": "Loop ativado",
"LoopDisabled": "Loop desativado"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Limpar concluído",
"showMore": "Mostrar mais",
"showLess": "Mostrar menos",
"openParentFolder": "Abrir a pasta principal"
"openParentFolder": "Abrir a pasta principal",
"selectedCount": "Número de itens selecionados para executar uma ação"
},
"download": {
"downloadFile": "Descarregar ficheiro",
@@ -84,15 +85,15 @@
"click": "selecionar pasta ou ficheiro",
"ctrl": {
"click": "selecionar várias pastas e ficheiros",
"f": "pesquisar",
"s": "guardar um ficheiro ou descarrega a pasta em que está a navegar"
"d": "descarregar itens selecionados"
},
"del": "eliminar os ficheiros selecionados",
"doubleClick": "abrir pasta ou ficheiro",
"esc": "limpar seleção e/ou fechar menu",
"f1": "esta informação",
"f2": "alterar nome do ficheiro",
"help": "Ajuda"
"help": "Ajuda",
"f1": "mostrar prompt de ajuda",
"description": "Pode ver as opções básicas de navegação abaixo. Para obter informações adicionais, visite",
"wiki": "Wiki do FileBrowser Quantum"
},
"languages": {
"he": "עברית",
@@ -134,7 +135,6 @@
"prompts": {
"copy": "Copiar",
"copyMessage": "Escolha um lugar para onde copiar os ficheiros:",
"deleteMessageMultiple": "Quer eliminar {count} ficheiro(s)?",
"deleteMessageSingle": "Quer eliminar esta pasta/ficheiro?",
"deleteTitle": "Eliminar ficheiros",
"displayName": "Nome:",
@@ -142,7 +142,6 @@
"downloadMessage": "Escolha o formato do ficheiro que quer descarregar.",
"error": "Algo correu mal",
"fileInfo": "Informação do ficheiro",
"lastModified": "Última alteração{suffix}",
"move": "Mover",
"moveMessage": "Escolha uma nova casa para os seus ficheiros/pastas:",
"newArchetype": "Criar um novo post baseado num \"archetype\". O seu ficheiro será criado na pasta \"content\".",
@@ -150,10 +149,7 @@
"newDirMessage": "Escreva o nome da nova pasta.",
"newFile": "Novo ficheiro",
"newFileMessage": "Escreva o nome do novo ficheiro.",
"numberDirs": "Número de pastas{suffix}",
"numberFiles": "Número de ficheiros{suffix}",
"rename": "Alterar nome",
"renameMessage": "Insira um novo nome para",
"replace": "Substituir",
"replaceMessage": "Já existe um ficheiro com nome igual a um dos que está a tentar enviar. Quer substituí-lo?\n",
"schedule": "Agendar",
@@ -161,7 +157,6 @@
"show": "Mostrar",
"size": "Tamanho",
"upload": "Carregar",
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
"deleteUserMessage": "Tem a certeza de que pretende apagar este utilizador?",
"selected": "selecionado",
"filesSelected": "ficheiros selecionados",
@@ -172,10 +167,6 @@
"dragAndDrop": "Arrastar e largar ficheiros aqui",
"completed": "Concluído",
"conflictsDetected": "Conflitos detectados",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Nome inválido fornecido: não pode conter \"/\" ou \"\\\"",
"source": "Source{suffix}",
"downloadsLimit": "Limite do número de descarregamentos",
"maxBandwidth": "Largura de banda máxima de descarregamento (kbps)",
"shareTheme": "Partilhar o tema",
@@ -186,22 +177,44 @@
"shareBanner": "Partilhar URL do banner",
"shareFavicon": "Partilhar URL do favicon",
"optionalPasswordHelp": "Se estiver definida, os utilizadores têm de introduzir esta palavra-passe para ver o conteúdo partilhado.",
"viewMode": "Modo de visualização"
"viewMode": "Modo de visualização",
"renameMessage": "Introduzir um novo nome:",
"renameMessageInvalid": "Um ficheiro não pode conter \"/\" ou \"\\\"",
"source": "Fonte{suffix}",
"deleteMessageMultiple": "Tem a certeza de que pretende apagar o(s) ficheiro(s) {count}?",
"deleteMessageShare": "Tem a certeza de que pretende apagar esta partilha: {path} ?",
"lastModified": "Última modificação{suffix}",
"numberDirs": "Número de diretórios{suffix}",
"numberFiles": "Número de ficheiros{suffix}",
"renameMessageConflict": "Já existe um item com o nome \"{filename}\"!",
"uploadSettingsChunked": "Máximo de carregamentos simultâneos: {maxConcurrentUpload}, tamanho do pedaço: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Máximo de carregamentos simultâneos: {maxConcurrentUpload}, carregamento em pedaços desativado"
},
"search": {
"images": "Imagens",
"music": "Música",
"pdf": "PDF",
"pressToSearch": "Tecla Enter para pesquisar...",
"search": "Pesquisar...",
"typeToSearch": "Escrever para pesquisar...",
"types": "Tipos",
"video": "Vídeos",
"path": "Caminho:",
"smallerThan": "Mais pequeno do que:",
"largerThan": "Maior do que:",
"helpText1": "A pesquisa ocorre em cada caractere digitado (mínimo de 3 caracteres para termos de pesquisa).",
"helpText2": "O índice: A pesquisa utiliza o índice que é atualizado automaticamente no intervalo configurado (predefinição: 5 minutos). A pesquisa quando o programa acabou de ser iniciado pode resultar em resultados incompletos."
"helpText2": "O índice: A pesquisa utiliza o índice que é atualizado automaticamente no intervalo configurado (predefinição: 5 minutos). A pesquisa quando o programa acabou de ser iniciado pode resultar em resultados incompletos.",
"noResults": "Não foram encontrados resultados na pesquisa indexada.",
"onlyFolders": "Apenas pastas",
"onlyFiles": "Apenas ficheiros",
"showOptions": "Mostrar opções",
"photos": "Fotos",
"audio": "Áudio",
"videos": "Vídeos",
"documents": "Documentos",
"archives": "Arquivos",
"number": "Número",
"searchContext": "Contexto de pesquisa: {context}",
"notEnoughCharacters": "Não há caracteres suficientes para pesquisar (min {minSearchLength})",
"typeToSearch": "Comece a escrever {minSearchLength} ou mais caracteres para iniciar a pesquisa."
},
"settings": {
"admin": "Admin",
@@ -214,10 +227,8 @@
"avoidChanges": "(deixe em branco para manter)",
"branding": "Marca",
"brandingDirectoryPath": "Caminho da pasta de marca",
"brandingHelp": "Pode personalizar a aparência do seu Navegador de Ficheiros, alterar o nome, substituindo o logótipo, adicionando estilos personalizados e mesmo desativando links externos para o GitHub.\nPara mais informações sobre marca personalizada, por favor veja {0}.",
"changePassword": "Alterar palavra-passe",
"commandRunner": "Execução de comandos",
"commandRunnerHelp": "Aqui pode definir comandos que são executados nos eventos nomeados. Tem de escrever um por linha. As variáveis de ambiente {0} e {1} estarão disponíveis, sendo {0} relativo a {1}. Para mais informações sobre esta funcionalidade e as variáveis de ambiente, veja {2}.",
"commandsUpdated": "Comandos atualizados!",
"createUserDir": "Criar automaticamente a pasta de início ao adicionar um novo utilizador",
"customStylesheet": "Folha de estilos personalizada",
@@ -300,7 +311,14 @@
"adminOptions": "Opções do utilizador administrador",
"shareDuration": "Expirações",
"searchOptions": "Opções de pesquisa",
"downloads": "Transferências"
"downloads": "Transferências",
"systemAdmin": "Sistema e administração",
"configViewer": "Visualizador de configuração",
"configViewerShowFull": "Mostrar configuração completa",
"configViewerShowComments": "Mostrar comentários",
"configViewerLoadConfig": "Configuração de carga",
"brandingHelp": "É possível personalizar a aparência da instância do Navegador de arquivos alterando seu nome, substituindo o logotipo, adicionando estilos personalizados e até mesmo desativando links externos para o GitHub.\nPara obter mais informações sobre a marca personalizada, consulte {0}.",
"commandRunnerHelp": "Aqui pode definir comandos que são executados nos eventos nomeados. Deve escrever um por linha. As variáveis de ambiente {0} e {1} estarão disponíveis, sendo {0} relativo a {1}. Para obter mais informações sobre este recurso e as variáveis de ambiente disponíveis, leia {2}."
},
"sidebar": {
"help": "Ajuda",
@@ -358,12 +376,21 @@
"hideNavButtons": "Ocultar botões de navegação",
"hideNavButtonsDescription": "Oculte os botões de navegação na barra de navegação na partilha para criar um aspeto minimalista.",
"viewModeDescription": "Esquema predefinido para a página partilhada.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Foi-lhe enviada uma ação para visualizar ou descarregar.",
"disableShareCard": "Desativar o cartão de partilha",
"disableShareCardDescription": "Desativar o cartão de partilha na página partilhada na barra lateral ou na parte superior da página no telemóvel.",
"disableSidebar": "Desativar a barra lateral",
"disableSidebarDescription": "Desativar a barra lateral na página partilhada."
"disableSidebarDescription": "Desativar a barra lateral na página partilhada.",
"enforceDarkLightMode": "Aplicar o modo de tema",
"enforceDarkLightModeDescription": "Força um modo de tema específico (escuro ou claro) para esta partilha, substituindo as preferências do utilizador.",
"default": "Não impor um modo",
"dark": "Escuro",
"light": "Luz",
"enableOnlyOffice": "Ativar o visualizador OnlyOffice",
"enableOnlyOfficeDescription": "Permitir a visualização de ficheiros de escritório utilizando o OnlyOffice nesta partilha.",
"enableOnlyOfficeEditing": "Ativar a edição OnlyOffice",
"enableOnlyOfficeEditingDescription": "Permitir a edição de ficheiros do Office utilizando o OnlyOffice nesta partilha.",
"titleDefault": "Ficheiros partilhados - {title}"
},
"api": {
"title": "Chaves da API",
@@ -478,7 +505,13 @@
"defaultThemeDescription": "default - O tema predefinido",
"customTheme": "Escolha o seu tema",
"showSelectMultiple": "Mostrar seleção múltipla no menu de contexto no ambiente de trabalho",
"showSelectMultipleDescription": "Por predefinição, o menu de contexto do ambiente de trabalho não tem a opção \"selecionar vários\" como no telemóvel. Esta opção mostra-a sempre no menu de contexto."
"showSelectMultipleDescription": "Por predefinição, o menu de contexto do ambiente de trabalho não tem a opção \"selecionar vários\" como no telemóvel. Esta opção mostra-a sempre no menu de contexto.",
"defaultMediaPlayer": "Utilize o leitor multimédia nativo do seu navegador",
"defaultMediaPlayerDescription": "Utilize o leitor multimédia nativo do seu navegador para ficheiros de vídeo e áudio em vez do leitor multimédia 'vue-plyr' incluído no FileBrowser.",
"debugOfficeEditor": "Ativar o modo de depuração do OnlyOffice",
"debugOfficeEditorDescription": "Mostrar um popup de depuração com informações adicionais no editor OnlyOffice.",
"fileViewerOptions": "Opções do visualizador de ficheiros",
"themeAndLanguage": "Tema e linguagem"
},
"general": {
"name": "Nome",
@@ -513,15 +546,19 @@
"name": "Nome",
"accessManagement": "Gestão do acesso",
"all": "Recusar tudo",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Isto mostra o que acontece quando um utilizador tenta aceder a um caminho onde não existem regras de acesso específicas. \"Permitir\" significa que os utilizadores podem aceder por predefinição, \"Negar\" significa que os utilizadores são bloqueados por predefinição, a menos que sejam explicitamente autorizados."
"defaultBehaviorDescription": "Isto mostra o que acontece quando um utilizador tenta aceder a um caminho onde não existem regras de acesso específicas. \"Permitir\" significa que os utilizadores podem aceder por predefinição, \"Negar\" significa que os utilizadores são bloqueados por predefinição, a menos que sejam explicitamente autorizados.",
"defaultBehavior": "Ação por defeito{suffix}"
},
"editor": {
"uninitialized": "Falha ao inicializar o editor. Por favor, recarregue.",
"saveFailed": "Falha ao guardar o ficheiro. Por favor, tente novamente.",
"saveAborted": "Gravação abortada. O nome do ficheiro não corresponde ao ficheiro ativo.",
"saveDisabled": "A gravação de ficheiros partilhados não é suportada.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Operação de gravação abortada. O ficheiro ativo da aplicação ({activeFile}) não corresponde ao ficheiro que está a tentar guardar ({tryingToSave}).",
"syncFailed": "não conseguiu sincronizar o estado com a rota para \"{filename}\" após 5 tentativas. Abortar a configuração do editor para evitar a corrupção de dados."
},
"player": {
"LoopEnabled": "Loop ativado",
"LoopDisabled": "Loop desativado"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Golire finalizată",
"showMore": "Arată mai multe",
"showLess": "Arată mai puțin",
"openParentFolder": "Deschideți folderul părinte"
"openParentFolder": "Deschideți folderul părinte",
"selectedCount": "Numărul de elemente selectate pentru a efectua o acțiune"
},
"download": {
"downloadFile": "Descarcă fișier",
@@ -84,15 +85,15 @@
"click": "alege fișier sau director",
"ctrl": {
"click": "alege multiple fișiere sau directoare",
"f": "deschide căutarea",
"s": "salvează un fișier sau descarcă directorul în care te afli"
"d": "descărcați elementele selectate"
},
"del": "șterge elementele selectate",
"doubleClick": "deschide un fișier sau director",
"esc": "elibereaza selecția și/sau închide fereastra",
"f1": "această informație",
"f2": "redenumește fișierul",
"help": "Ajutor"
"help": "Ajutor",
"f1": "arată ajutor prompt",
"description": "Puteți vizualiza mai jos opțiunile de navigație de bază. Pentru informații suplimentare, vă rugăm să vizitați",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -134,7 +135,6 @@
"prompts": {
"copy": "Copiază",
"copyMessage": "Alege unde vrei să copiezi fișierele:",
"deleteMessageMultiple": "Ești sigur că vrei să ștergi aceste {count} fișier(e)?",
"deleteMessageSingle": "Ești sigur că vrei să ștergi acest fișier/director?",
"deleteTitle": "Șterge fișiere",
"displayName": "Nume afișat:",
@@ -142,7 +142,6 @@
"downloadMessage": "Alege formatul în care vrei să descarci.",
"error": "Ceva n-a funcționat cum trebuie",
"fileInfo": "Informații fișier",
"lastModified": "Ultima modificare{suffix}",
"move": "Mută",
"moveMessage": "Alege destinația:",
"newArchetype": "Crează o nouă postare pe baza unui șablon. Fișierul va fi creat in directorul conținut.",
@@ -150,10 +149,7 @@
"newDirMessage": "Scrie numele noului director.",
"newFile": "Fișier nou",
"newFileMessage": "Scrie numele noului fișier.",
"numberDirs": "Numărul directoarelor{suffix}",
"numberFiles": "Numărul fișierelor{suffix}",
"rename": "Redenumește",
"renameMessage": "Redactează un nou nume pentru",
"replace": "Înlocuiește",
"replaceMessage": "Unul din fișierele încărcate intră în conflict din cauza denumrii. Vrei să înlocuiești fișierul existent?\n",
"schedule": "Programare",
@@ -161,7 +157,6 @@
"show": "Arată",
"size": "Dimensiune",
"upload": "Încărcare",
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
"deleteUserMessage": "Sunteți sigur că doriți să ștergeți acest utilizator?",
"filesSelected": "fișiere selectate",
"optionalPassword": "Parolă opțională",
@@ -172,10 +167,6 @@
"dragAndDrop": "Drag and drop fișiere aici",
"completed": "Completat",
"conflictsDetected": "Conflicte detectate",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Nume invalid furnizat: nu poate conține \"/\" sau \"\\\"",
"source": "Source{suffix}",
"downloadsLimit": "Limita numărului de descărcări",
"maxBandwidth": "Lățime de bandă maximă de descărcare (kbps)",
"shareTheme": "Temă de partajare",
@@ -186,22 +177,44 @@
"shareBanner": "Partajați URL-ul bannerului",
"shareFavicon": "Partajați URL favicon",
"optionalPasswordHelp": "Dacă este setată, utilizatorii trebuie să introducă această parolă pentru a vizualiza conținutul partajat.",
"viewMode": "Mod de vizualizare"
"viewMode": "Mod de vizualizare",
"renameMessage": "Introduceți un nume nou:",
"renameMessageInvalid": "Un fișier nu poate conține \"/\" sau \"\\\"",
"source": "Sursa{suffix}",
"deleteMessageMultiple": "Sunteți sigur că doriți să eliminați fișierul (fișierele) {count}?",
"deleteMessageShare": "Sunteți sigur că doriți să ștergeți această partajare: {path} ?",
"lastModified": "Ultima modificare{suffix}",
"numberDirs": "Număr de directoare{suffix}",
"numberFiles": "Număr de fișiere{suffix}",
"renameMessageConflict": "Un element numit \"{filename}\" există deja!",
"uploadSettingsChunked": "Încărcări simultane maxime: {maxConcurrentUpload}, dimensiunea bucăților: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Încărcări simultane maxime: {maxConcurrentUpload}, încărcare fragmentată dezactivată"
},
"search": {
"images": "Imagini",
"music": "Muzică",
"pdf": "PDF",
"pressToSearch": "Apasă enter pentru a căuta...",
"search": "Caută...",
"typeToSearch": "Scrie pentru a căuta...",
"types": "Tipuri",
"video": "Video",
"path": "Calea:",
"smallerThan": "Mai mic decât:",
"largerThan": "Mai mare decât:",
"helpText1": "Căutarea are loc pe fiecare caracter tastat (minim 3 caractere pentru termenii de căutare).",
"helpText2": "Indexul: Căutarea utilizează indexul care se actualizează automat la intervalul configurat (implicit: 5 minute). Căutarea atunci când programul tocmai a început poate duce la rezultate incomplete."
"helpText2": "Indexul: Căutarea utilizează indexul care se actualizează automat la intervalul configurat (implicit: 5 minute). Căutarea atunci când programul tocmai a început poate duce la rezultate incomplete.",
"noResults": "Nu s-au găsit rezultate în căutarea indexată.",
"onlyFolders": "Numai foldere",
"onlyFiles": "Numai fișiere",
"showOptions": "Afișare opțiuni",
"photos": "Fotografii",
"audio": "Audio",
"videos": "Video",
"documents": "Documente",
"archives": "Arhive",
"number": "Număr",
"searchContext": "Context de căutare: {context}",
"notEnoughCharacters": "Nu sunt suficiente caractere pentru căutare (min {minSearchLength})",
"typeToSearch": "Începeți să tastați {minSearchLength} sau mai multe caractere pentru a începe căutarea."
},
"settings": {
"admin": "Admin",
@@ -214,10 +227,8 @@
"avoidChanges": "(lasă gol pentru a nu schimba)",
"branding": "Branding",
"brandingDirectoryPath": "Calea către directorul de branding",
"brandingHelp": "Poți personaliza cum arată instanța ta de File Browser modificându-i numele, înlocuindu-i logo-ul, adăugându-i stiluri personalizate si chiar dezactivând linkurile catre GitHub.\nPentru mai multe informații despre branding fă click aici",
"changePassword": "Schimbă parola",
"commandRunner": "Rulează comenzi",
"commandRunnerHelp": "Aici poti seta comenzile care sunt executate in evenimente. Trebuie să scrii una pe linie. Variabilele de mediu {0} și {1} vor fi disponile, {0} fiind relativă la {1}. Pentru mai multe informații despre acest feature si variabilele de mediu disponibile, cititi {2}.",
"commandsUpdated": "Comenzi actualizate!",
"createUserDir": "Auto create user home dir while adding new user",
"customStylesheet": "CSS personalizat",
@@ -300,7 +311,14 @@
"adminOptions": "Opțiuni utilizator administrator",
"shareDuration": "Expiră",
"searchOptions": "Opțiuni de căutare",
"downloads": "Descărcări"
"downloads": "Descărcări",
"systemAdmin": "Sistem și administrare",
"configViewer": "Vizualizator de configurare",
"configViewerShowFull": "Afișați configurația completă",
"configViewerShowComments": "Afișați comentariile",
"configViewerLoadConfig": "Configurare încărcare",
"brandingHelp": "Puteți personaliza aspectul instanței File Browser prin schimbarea numelui, înlocuirea logo-ului, adăugarea de stiluri personalizate și chiar dezactivarea linkurilor externe către GitHub.\nPentru mai multe informații despre brandingul personalizat, consultați {0}.",
"commandRunnerHelp": "Aici puteți seta comenzi care sunt executate în cadrul evenimentelor numite. Trebuie să scrieți câte una pe linie. Variabilele de mediu {0} și {1} vor fi disponibile, {0} fiind relativ la {1}. Pentru mai multe informații despre această caracteristică și variabilele de mediu disponibile, vă rugăm să citiți {2}."
},
"sidebar": {
"help": "Ajutor",
@@ -358,12 +376,21 @@
"hideNavButtons": "Ascundeți butoanele de navigare",
"hideNavButtonsDescription": "Ascundeți butoanele de navigare de pe bara de navigare din share pentru a crea un aspect minimalist.",
"viewModeDescription": "Layout implicit pentru pagina partajată.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "V-a fost trimisă o acțiune pentru a o vizualiza sau descărca.",
"disableShareCard": "Dezactivați cardul de partajare",
"disableShareCardDescription": "Dezactivați cardul de partajare pe pagina partajată în bara laterală sau în partea de sus a paginii pe mobil.",
"disableSidebar": "Dezactivați bara laterală",
"disableSidebarDescription": "Dezactivați bara laterală de pe pagina partajată."
"disableSidebarDescription": "Dezactivați bara laterală de pe pagina partajată.",
"enforceDarkLightMode": "Impuneți modul tematic",
"enforceDarkLightModeDescription": "Forțează un anumit mod tematic (întunecat sau deschis) pentru această partajare, trecând peste preferințele utilizatorului.",
"default": "Nu impuneți un mod",
"dark": "Întuneric",
"light": "Lumină",
"enableOnlyOffice": "Activați vizualizatorul OnlyOffice",
"enableOnlyOfficeDescription": "Permiteți vizualizarea fișierelor office utilizând OnlyOffice în această partajare.",
"enableOnlyOfficeEditing": "Activați editarea OnlyOffice",
"enableOnlyOfficeEditingDescription": "Permiteți editarea fișierelor de birou utilizând OnlyOffice în această partajare.",
"titleDefault": "Fișiere partajate - {title}"
},
"api": {
"title": "Chei API:",
@@ -447,8 +474,8 @@
"totalDenied": "Refuzat",
"totalAllowed": "Permise",
"all": "Refuzați toate",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Aceasta arată ce se întâmplă atunci când un utilizator încearcă să acceseze o cale unde nu există reguli specifice de acces. \"Allow\" înseamnă că utilizatorii pot accesa în mod implicit, \"Deny\" înseamnă că utilizatorii sunt blocați în mod implicit dacă nu li se permite în mod explicit."
"defaultBehaviorDescription": "Aceasta arată ce se întâmplă atunci când un utilizator încearcă să acceseze o cale unde nu există reguli specifice de acces. \"Allow\" înseamnă că utilizatorii pot accesa în mod implicit, \"Deny\" înseamnă că utilizatorii sunt blocați în mod implicit dacă nu li se permite în mod explicit.",
"defaultBehavior": "Acțiune implicită{suffix}"
},
"fileLoading": {
"title": "Încărcări și descărcări",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - Tema implicită",
"customTheme": "Alege-ți tema",
"showSelectMultiple": "Afișarea selecției multiple în meniul contextual de pe desktop",
"showSelectMultipleDescription": "În mod implicit, meniul contextual de pe desktop nu are opțiunea \"select multiple\", ca pe mobil. Această opțiune se afișează întotdeauna în meniul contextual."
"showSelectMultipleDescription": "În mod implicit, meniul contextual de pe desktop nu are opțiunea \"select multiple\", ca pe mobil. Această opțiune se afișează întotdeauna în meniul contextual.",
"defaultMediaPlayer": "Utilizați playerul media nativ al browserului dvs.",
"defaultMediaPlayerDescription": "Utilizați playerul media nativ al browserului dvs. pentru fișierele video și audio mai degrabă decât playerul media \"vue-plyr\" inclus în FileBrowser.",
"debugOfficeEditor": "Activați modul de depanare OnlyOffice",
"debugOfficeEditorDescription": "Afișați o fereastră pop-up de depanare cu informații suplimentare în editorul OnlyOffice.",
"fileViewerOptions": "Opțiuni vizualizator de fișiere",
"themeAndLanguage": "Temă și limbaj"
},
"editor": {
"uninitialized": "Nu s-a reușit inițializarea editorului. Vă rugăm să reîncărcați.",
"saveFailed": "Nu ați reușit să salvați fișierul. Vă rugăm să încercați din nou.",
"saveAborted": "Salvare întreruptă. Numele fișierului nu corespunde fișierului activ.",
"saveDisabled": "Salvarea fișierelor partajate nu este acceptată.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Operațiunea de salvare a fost întreruptă. Fișierul activ al aplicației ({activeFile}) nu corespunde cu fișierul pe care încercați să îl salvați ({tryingToSave}).",
"syncFailed": "nu a reușit să sincronizeze starea cu ruta pentru \"{filename}\" după 5 încercări. Întreruperea configurării editorului pentru a preveni coruperea datelor."
},
"player": {
"LoopEnabled": "Bucla activată",
"LoopDisabled": "Bucla dezactivată"
}
}
+67 -30
View File
@@ -49,7 +49,8 @@
"clearCompleted": "Очистка завершена",
"showMore": "Показать еще",
"showLess": "Показать меньше",
"openParentFolder": "Откройте родительскую папку"
"openParentFolder": "Откройте родительскую папку",
"selectedCount": "Количество элементов, выбранных для выполнения действия"
},
"download": {
"downloadFile": "Скачать файл",
@@ -84,15 +85,15 @@
"click": "выбрать файл или каталог",
"ctrl": {
"click": "выбрать несколько файлов или каталогов",
"f": "открыть поиск",
"s": "скачать файл или текущий каталог"
"d": "скачать выбранные элементы"
},
"del": "удалить выбранные элементы",
"doubleClick": "открыть файл или каталог",
"esc": "очистить выделение и/или закрыть окно",
"f1": "помощь",
"f2": "переименовать файл",
"help": "Помощь"
"help": "Помощь",
"f1": "показать подсказку",
"description": "Ниже представлены основные варианты навигации. Для получения дополнительной информации, пожалуйста, посетите",
"wiki": "FileBrowser Quantum Wiki"
},
"languages": {
"he": "עברית",
@@ -134,16 +135,13 @@
"prompts": {
"copy": "Копировать",
"copyMessage": "Копировать в:",
"deleteMessageMultiple": "Удалить эти файлы ({count})?",
"deleteMessageSingle": "Удалить этот файл/каталог?",
"deleteMessageShare": "Удалить этот общий файл/каталог ({path})?",
"deleteTitle": "Удалить файлы",
"displayName": "Отображаемое имя:",
"download": "Скачать файлы",
"downloadMessage": "Выберите формат в котором хотите скачать.",
"error": "Ошибка",
"fileInfo": "Информация о файле",
"lastModified": "Последнее изменение{suffix}",
"move": "Переместить",
"moveMessage": "Переместить в:",
"newArchetype": "Создайте новую запись на основе архетипа. Файл будет создан в каталоге.",
@@ -151,10 +149,7 @@
"newDirMessage": "Имя нового каталога.",
"newFile": "Новый файл",
"newFileMessage": "Имя нового файла.",
"numberDirs": "Количество каталогов{suffix}",
"numberFiles": "Количество файлов{suffix}",
"rename": "Переименовать",
"renameMessage": "Новое имя",
"replace": "Заменить",
"replaceMessage": "Имя одного из загружаемых файлов совпадает с уже существующим файлом. Вы хотите заменить существующий?\n",
"schedule": "Планировка",
@@ -172,10 +167,6 @@
"dragAndDrop": "Перетащите файлы сюда",
"completed": "Завершено",
"conflictsDetected": "Обнаруженные конфликты",
"uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled",
"invalidName": "Неверное имя: не может содержать \"/\" или \"\\\".",
"source": "Source{suffix}",
"downloadsLimit": "Ограничение на количество загрузок",
"maxBandwidth": "Максимальная пропускная способность загрузки (кб/с)",
"shareTheme": "Поделиться темой",
@@ -186,22 +177,44 @@
"shareBanner": "Поделиться URL-адресом баннера",
"shareFavicon": "Поделиться URL-адресом favicon",
"optionalPasswordHelp": "Если установлен, пользователи должны ввести этот пароль для просмотра общего содержимого.",
"viewMode": "Режим просмотра"
"viewMode": "Режим просмотра",
"renameMessage": "Введите новое имя:",
"renameMessageInvalid": "Файл не может содержать \"/\" или \"\\\".",
"source": "Источник{suffix}",
"deleteMessageMultiple": "Вы уверены, что хотите удалить файл(ы) {count}?",
"deleteMessageShare": "Вы уверены, что хотите удалить этот ресурс: {path} ?",
"lastModified": "Последнее изменение{suffix}",
"numberDirs": "Количество каталогов{suffix}",
"numberFiles": "Количество файлов{suffix}",
"renameMessageConflict": "Элемент с именем \"{filename}\" уже существует!",
"uploadSettingsChunked": "Максимальное количество одновременных загрузок: {maxConcurrentUpload}, размер чанка: {uploadChunkSizeMb} MB",
"uploadSettingsNoChunk": "Максимальное количество одновременных загрузок: {maxConcurrentUpload}, загрузка в куски отключена"
},
"search": {
"images": "Изображения",
"music": "Музыка",
"pdf": "PDF",
"pressToSearch": "Нажмите Enter для поиска ...",
"search": "Поиск...",
"typeToSearch": "Введите имя файла ...",
"types": "Типы",
"video": "Видео",
"path": "Путь:",
"smallerThan": "Меньше, чем:",
"largerThan": "Больше, чем:",
"helpText1": "Поиск осуществляется по каждому введенному символу (минимум 3 символа для поисковых запросов).",
"helpText2": "Индекс: Для поиска используется индекс, который автоматически обновляется с заданным интервалом (по умолчанию: 5 минут). Поиск, когда программа только что запущена, может привести к неполным результатам."
"helpText2": "Индекс: Для поиска используется индекс, который автоматически обновляется с заданным интервалом (по умолчанию: 5 минут). Поиск, когда программа только что запущена, может привести к неполным результатам.",
"noResults": "В проиндексированном поиске результатов не найдено.",
"onlyFolders": "Только папки",
"onlyFiles": "Только файлы",
"showOptions": "Показать параметры",
"photos": "Фотографии",
"audio": "Аудио",
"videos": "Видео",
"documents": "Документы",
"archives": "Архивы",
"number": "Номер",
"searchContext": "Контекст поиска: {context}",
"notEnoughCharacters": "Недостаточно символов для поиска (мин. {minSearchLength})",
"typeToSearch": "Начните вводить {minSearchLength} или более символов, чтобы начать поиск."
},
"settings": {
"admin": "Админ",
@@ -214,10 +227,8 @@
"avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)",
"branding": "Брендинг",
"brandingDirectoryPath": "Путь к каталогу брендов",
"brandingHelp": "Вы можете настроить внешний вид файлового браузера, изменив его имя, заменив логотип, добавив собственные стили и даже отключив внешние ссылки на GitHub.\nДополнительную информацию о персонализированном брендинге можно найти на странице {0}.",
"changePassword": "Изменение пароля",
"commandRunner": "Запуск команд",
"commandRunnerHelp": "Здесь вы можете установить команды, которые будут выполняться в указанных событиях. Вы должны указать по одной команде в каждой строке. Переменные среды {0} и {1} будут доступны, будучи {0} относительно {1}. Дополнительные сведения об этой функции и доступных переменных среды см. В {2}.",
"commandsUpdated": "Команды обновлены!",
"createUserDir": "Автоматическое создание домашнего каталога пользователя при добавлении нового пользователя",
"customStylesheet": "Свой стиль",
@@ -300,7 +311,14 @@
"adminOptions": "Параметры пользователя администратора",
"shareDuration": "Срок действия:",
"searchOptions": "Параметры поиска",
"downloads": "Скачать"
"downloads": "Скачать",
"systemAdmin": "Система и администрирование",
"configViewer": "Программа просмотра конфигурации",
"configViewerShowFull": "Показать полную конфигурацию",
"configViewerShowComments": "Показать комментарии",
"configViewerLoadConfig": "Конфигурация нагрузки",
"brandingHelp": "Вы можете настроить внешний вид экземпляра File Browser, изменив его название, заменив логотип, добавив пользовательские стили и даже отключив внешние ссылки на GitHub.\nБолее подробную информацию о пользовательском брендинге можно найти на сайте {0}.",
"commandRunnerHelp": "Здесь вы можете задать команды, которые будут выполняться в названных событиях. Вы должны написать по одной команде в строке. Будут доступны переменные окружения {0} и {1}, причем {0} будет относиться к {1}. Для получения дополнительной информации об этой функции и доступных переменных окружения, пожалуйста, прочтите {2}."
},
"sidebar": {
"help": "Помощь",
@@ -358,12 +376,21 @@
"hideNavButtons": "Скрыть кнопки навигации",
"hideNavButtonsDescription": "Спрячьте кнопки навигации на навигационной панели в ресурсе, чтобы создать минималистичный вид.",
"viewModeDescription": "Макет по умолчанию для общей страницы.",
"titleDefault": "Shared files - {title}",
"descriptionDefault": "Вам отправлена акция для просмотра или загрузки.",
"disableShareCard": "Отключение карты доступа",
"disableShareCardDescription": "Отключите карточку общего доступа на странице общего доступа в боковой панели или в верхней части страницы на мобильных устройствах.",
"disableSidebar": "Отключить боковую панель",
"disableSidebarDescription": "Отключите боковую панель на общей странице."
"disableSidebarDescription": "Отключите боковую панель на общей странице.",
"enforceDarkLightMode": "Включение режима темы",
"enforceDarkLightModeDescription": "Принудительно устанавливает определенный режим темы (темный или светлый) для этого ресурса, отменяя предпочтения пользователя.",
"default": "Не вводите режим",
"dark": "Темнота",
"light": "Свет",
"enableOnlyOffice": "Включите программу просмотра OnlyOffice",
"enableOnlyOfficeDescription": "Разрешите просмотр офисных файлов с помощью OnlyOffice в этом ресурсе.",
"enableOnlyOfficeEditing": "Включить редактирование OnlyOffice",
"enableOnlyOfficeEditingDescription": "Разрешите редактирование офисных файлов с помощью OnlyOffice в этом ресурсе.",
"titleDefault": "Общие файлы - {title}"
},
"api": {
"title": "Ключи API:",
@@ -447,8 +474,8 @@
"totalDenied": "Отказано",
"totalAllowed": "Разрешено",
"all": "Запретить все",
"defaultBehavior": "Default action{suffix}",
"defaultBehaviorDescription": "Здесь показано, что происходит, когда пользователь пытается получить доступ к пути, для которого не существует специальных правил доступа. 'Allow' означает, что пользователи могут получить доступ по умолчанию, 'Deny' означает, что пользователи блокируются по умолчанию, если только они не разрешены явным образом."
"defaultBehaviorDescription": "Здесь показано, что происходит, когда пользователь пытается получить доступ к пути, для которого не существует специальных правил доступа. 'Allow' означает, что пользователи могут получить доступ по умолчанию, 'Deny' означает, что пользователи блокируются по умолчанию, если только они не разрешены явным образом.",
"defaultBehavior": "Действие по умолчанию{suffix}"
},
"fileLoading": {
"title": "Загрузка и скачивание",
@@ -514,14 +541,24 @@
"defaultThemeDescription": "default - Тема по умолчанию",
"customTheme": "Выберите тему",
"showSelectMultiple": "Отображение выбора нескольких в контекстном меню на рабочем столе",
"showSelectMultipleDescription": "По умолчанию в контекстном меню рабочего стола нет опции \"выбрать несколько\", как в мобильном. Эта опция всегда отображается в контекстном меню."
"showSelectMultipleDescription": "По умолчанию в контекстном меню рабочего стола нет опции \"выбрать несколько\", как в мобильном. Эта опция всегда отображается в контекстном меню.",
"defaultMediaPlayer": "Используйте встроенный медиаплеер вашего браузера",
"defaultMediaPlayerDescription": "Для воспроизведения видео- и аудиофайлов используйте штатный медиаплеер браузера, а не медиаплеер 'vue-plyr', входящий в комплект поставки FileBrowser.",
"debugOfficeEditor": "Включить режим отладки OnlyOffice",
"debugOfficeEditorDescription": "Отображение всплывающего окна отладки с дополнительной информацией в редакторе OnlyOffice.",
"fileViewerOptions": "Параметры просмотра файлов",
"themeAndLanguage": "Тема и язык"
},
"editor": {
"uninitialized": "Не удалось инициализировать редактор. Пожалуйста, перезагрузитесь.",
"saveFailed": "Не удалось сохранить файл. Пожалуйста, попробуйте еще раз.",
"saveAborted": "Сохранение прервано. Имя файла не совпадает с именем активного файла.",
"saveDisabled": "Сохранение общих файлов не поддерживается.",
"saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).",
"syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption."
"saveAbortedMessage": "Операция сохранения прервана. Активный файл приложения ({activeFile}) не совпадает с файлом, который вы пытаетесь сохранить ({tryingToSave}).",
"syncFailed": "не удалось синхронизировать состояние с маршрутом для \"{filename}\" после 5 попыток. Прерываю настройку редактора, чтобы предотвратить повреждение данных."
},
"player": {
"LoopEnabled": "Петля включена",
"LoopDisabled": "Петля отключена"
}
}
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more