mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-13 12:41:59 -05:00
Compare commits
161
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78397fc415 | ||
|
|
5d01e1b1b8 | ||
|
|
3c0632d682 | ||
|
|
85bde31ab8 | ||
|
|
fc42dab33a | ||
|
|
1e66561366 | ||
|
|
5a2e1f38b3 | ||
|
|
c2232f436a | ||
|
|
b0b900c7ed | ||
|
|
a8c6d05833 | ||
|
|
0c23cc8324 | ||
|
|
fe02b9422b | ||
|
|
ad2aed47bc | ||
|
|
5b13673fff | ||
|
+4 |
5c369404e8 | ||
|
+4 |
af8cde651b | ||
|
+4 |
9e95977366 | ||
|
|
063012477e | ||
|
|
ad04c6833c | ||
|
|
45e8da383b | ||
|
+4 |
4167d3e420 | ||
|
|
23f9d98dde | ||
|
+4 |
13162e20e5 | ||
|
+4 |
d2214cc01c | ||
|
+4 |
b7835de1c9 | ||
|
|
a806476686 | ||
|
|
0296216787 | ||
|
|
84ec5c177d | ||
|
+4 |
e72c47dc5e | ||
|
|
edaa8c536d | ||
|
|
52d57017f2 | ||
|
|
1dd219ad18 | ||
|
|
e584c30225 | ||
|
|
801c029b03 | ||
|
|
bf46e977c4 | ||
|
|
c02a994880 | ||
|
|
2cf352876c | ||
|
|
fb2c97d05a | ||
|
|
0bd58c14ee | ||
|
|
09376139f1 | ||
|
|
59495b7f2f | ||
|
|
d258333f26 | ||
|
|
fe57dd5edc | ||
|
|
b96636279e | ||
|
|
3025442ff8 | ||
|
|
715a044af1 | ||
|
|
a28d515b90 | ||
|
+4 |
990d53fa95 | ||
|
|
f0068d0765 | ||
|
|
8fd13a40bc | ||
|
|
941841fca1 | ||
|
|
0235546a8f | ||
|
|
66482cd1d5 | ||
|
|
d26189968f | ||
|
|
eb518bac56 | ||
|
|
d82e7f2986 | ||
|
|
7cd3f8374e | ||
|
|
4e0f0453f3 | ||
|
|
98246b2981 | ||
|
|
d2c07ffd3c | ||
|
|
43cbadbeb8 | ||
|
|
25dc426522 | ||
|
|
27dbd509b2 | ||
|
|
3716c4e4c6 | ||
|
|
7aabdadf3e | ||
|
|
264de1ce88 | ||
|
|
4aae24cf11 | ||
|
|
0968c44d22 | ||
|
|
a15fe4eaa5 | ||
|
|
a433a8a8d4 | ||
|
|
b5219ae6b6 | ||
|
|
d9f91c7c65 | ||
|
|
4a85746850 | ||
|
|
3ae8aede25 | ||
|
|
3990890721 | ||
|
|
726116d831 | ||
|
|
8274f2c715 | ||
|
|
32969d0777 | ||
|
|
8f25186136 | ||
|
|
e18a73c97a | ||
|
|
e5cb88ba5d | ||
|
|
50b9ee8b31 | ||
|
|
a0e266fadf | ||
|
|
377ae33e1b | ||
|
|
e14f2d7a4b | ||
|
|
fac091f1b3 | ||
|
|
5330d6b093 | ||
|
|
692f99251d | ||
|
|
31e70519c1 | ||
|
|
0b88a90048 | ||
|
|
79d7f07943 | ||
|
|
b562edaaeb | ||
|
|
33ede042da | ||
|
|
036a0ed6c8 | ||
|
|
1842ff561b | ||
|
|
aed9f7c135 | ||
|
|
c6600a0665 | ||
|
|
3e3d3decc6 | ||
|
|
c58b298eb5 | ||
|
|
98ba8095db | ||
|
|
68fb573059 | ||
|
|
5747c40dca | ||
|
|
598bd366db | ||
|
|
e3be3300dc | ||
|
|
416aa75446 | ||
|
|
cfb3b9e869 | ||
|
|
166f1bf232 | ||
|
|
451f9ac69f | ||
|
|
b2fa1780ed | ||
|
|
9255bc174b | ||
|
|
a3a074d7b6 | ||
|
|
aefe16b508 | ||
|
|
728ee72e2a | ||
|
|
e94075de6c | ||
|
|
c56bafb4e9 | ||
|
|
a6e045cc85 | ||
|
|
690685459e | ||
|
|
6f6244b2fb | ||
|
|
05fcd31909 | ||
|
|
a4107a734a | ||
|
|
991be79521 | ||
|
|
8755e82780 | ||
|
|
d1b3a045af | ||
|
|
b5b365c6a7 | ||
|
|
c33a876033 | ||
|
|
42778b92bb | ||
|
|
ff3b66b79d | ||
|
|
daedeef78d | ||
|
|
edba1a80ef | ||
|
|
77fb6bdabc | ||
|
|
97c37caa1c | ||
|
|
02b28284ca | ||
|
|
6a489c6f7c | ||
|
|
ba443398ca | ||
|
|
17ff68c00d | ||
|
|
cb71dd8940 | ||
|
|
764155d612 | ||
|
|
9013152d9d | ||
|
|
27bc000cda | ||
|
|
8f4e420e73 | ||
|
|
e4d8a5cf20 | ||
|
|
fa8ceb50e6 | ||
|
|
c8073db4e6 | ||
|
|
23fac83e7e | ||
|
|
f36ca31c7f | ||
|
|
c0466e96fc | ||
|
|
dc753fe326 | ||
|
|
c39a1937b4 | ||
|
|
5d38b1e66a | ||
|
|
e5e93b4b70 | ||
|
|
eaedbbe05e | ||
|
|
c6d5f32bd0 | ||
|
|
ee022bc4a9 | ||
|
|
daabefa6c3 | ||
|
|
86aebde566 | ||
|
|
a3b0fc0038 | ||
|
|
695364c2df | ||
|
|
58a01ff986 | ||
|
|
3298111560 | ||
|
|
3ca9f54de0 | ||
|
|
97d16d1663 |
No files matched your search
@@ -2,85 +2,6 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v1.2.2-stable
|
||||
|
||||
**Security**:
|
||||
- Patched Stored XSS in public share page via unsanitized share metadata (text/template misuse)
|
||||
- Patched Incomplete Remediation of CVE-2026-27611: Password-Protected Share Bypass via /public/api/share/info GHSA-525j-95gf-766f
|
||||
|
||||
**New Features**:
|
||||
- "Divider" option in sidebar links to add a text or divider between links (#1875)
|
||||
- shares offer a "go to source Location" sidebar link and button when editing a share.
|
||||
|
||||
**Notes**:
|
||||
- Share edit/delete permissions are scoped to the user's shares rather than global (#2050)
|
||||
- OIDC group claims accepted as map (#2084)
|
||||
|
||||
**BugFixes**:
|
||||
- fixed the requirement that the database path needed to be set in the config file, now it loads `FILEBROWSER_DATABASE` value by default, fallback to config file property.
|
||||
- Error downloading zipped directory: no such file or directory (users with scope) #2015
|
||||
|
||||
## v1.2.1-stable
|
||||
|
||||
**BugFixes**:
|
||||
- startup icon generation order issue (#2019)
|
||||
- login button on shares not showing (#2028)
|
||||
- removed file watcher link in context menu for shares (#2025)
|
||||
- Direct download link for single-file shares generates incorrect URL (#2026)
|
||||
- Deleting files causes error V1.2.0 #2039
|
||||
|
||||
## v1.2.7-beta
|
||||
|
||||
**BugFixes**:
|
||||
- head > title > infinitely duplicating titles (#2016)
|
||||
- Proxy auth not working with Nginx, stuck at logo (#2013)
|
||||
|
||||
## v1.2.6-beta
|
||||
|
||||
**Security**:
|
||||
- resolves https://github.com/gtsteffaniak/filebrowser/security/advisories/GHSA-8vrh-3pm2-v4v6
|
||||
|
||||
## v1.2.5-beta
|
||||
|
||||
**Notes**:
|
||||
- Dependency Updates for frontend and backend packages
|
||||
- Upgrade to go 1.26
|
||||
- file list prompt interaction behavior, single-click always selects, double-click navigates. (#1911)
|
||||
|
||||
**BugFixes**:
|
||||
- .ssa and .ass subtitle files get their styling removed during conversion (#1933)
|
||||
- custom icons issue
|
||||
- new database message for noauth (#1935)
|
||||
|
||||
## v1.2.4-beta
|
||||
|
||||
**Notes**:
|
||||
- Changed `/api/media/subtitles` api endpoint to better support subtitles.
|
||||
- Correcting some errors in French language (#1947)
|
||||
- add Dutch `nl.json` for frontend i18n -- thanks @Stephan-P (#1957)
|
||||
- share banner and icon images automatically serve a scaled down 1024x1024
|
||||
|
||||
**BugFixes**:
|
||||
- Can't access directories with space (and possibly other special characters) when browsing a public link share (#1956)
|
||||
- External ASS subtitles are not usable in web preview (#1933)
|
||||
- Filename blocking rename/move (#1950)
|
||||
- enforcedOtp: true blocks all user settings updates with HTTP 400 (#1962)
|
||||
- Share favicon URL not showing up and Share description causing endless loading (#1911)
|
||||
- folder sizes not updated for changes to filesystem outside of filebrowser (#1974)
|
||||
|
||||
## v1.2.3-beta
|
||||
|
||||
**Notes**:
|
||||
- Removed upload api behavior to assume paths ending in "/" are folders, strictly uses isDir query param
|
||||
- renamed public upload api query param from `targetPath` to `path`. see swagger docs.
|
||||
|
||||
**BugFixes**:
|
||||
- fix stuck motion preview popup window issue
|
||||
- Chunked uploads ignore User Scope and are saved to Source Root (#1894)
|
||||
- share delete with user scope issue
|
||||
- fix favicon pwa icon, having the PWA respect the favicon/loginIcon (#1899)
|
||||
- delete prompt icons show image previews
|
||||
|
||||
## v1.2.2-beta
|
||||
|
||||
**New Features**:
|
||||
@@ -114,7 +35,6 @@ All notable changes to this project will be documented in this file. For commit
|
||||
- added 75MB filesize limit for image previews
|
||||
- optimized concurrency for large vs small images
|
||||
|
||||
|
||||
## v1.2.1-beta
|
||||
|
||||
**New Features**:
|
||||
|
||||
@@ -6,6 +6,7 @@ WORKDIR /app
|
||||
COPY ./backend ./
|
||||
RUN apk update && apk add --no-cache gcc musl-dev upx
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOEXPERIMENT=greenteagc
|
||||
RUN go build -tags mupdf,musl -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
|
||||
|
||||
@@ -4,6 +4,7 @@ ARG REVISION
|
||||
WORKDIR /app
|
||||
COPY ./backend ./
|
||||
RUN apk update && apk add upx
|
||||
ENV GOEXPERIMENT=greenteagc
|
||||
RUN go build -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
|
||||
|
||||
@@ -2,6 +2,7 @@ package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -36,18 +37,6 @@ func processDirectoryMetadata(response *iteminfo.ExtendedFileInfo, idx *indexing
|
||||
metadataCount++
|
||||
}
|
||||
}
|
||||
// Process files concurrently using goroutines
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex // Protects processedCount
|
||||
|
||||
// Refresh directory to update folder sizes
|
||||
wg.Go(func() {
|
||||
indexPath := idx.MakeIndexPath(response.Path, true)
|
||||
err := idx.RefreshDirectory(indexPath, false)
|
||||
if err != nil {
|
||||
logger.Debugf("Failed to refresh directory for folder size update: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Set hasMetadata flag if there are files with potential metadata
|
||||
if metadataCount > 0 {
|
||||
@@ -61,6 +50,9 @@ func processDirectoryMetadata(response *iteminfo.ExtendedFileInfo, idx *indexing
|
||||
// Create a single shared FFmpegService instance for all files to coordinate concurrency
|
||||
sharedFFmpegService := ffmpeg.NewFFmpegService(10, false, "")
|
||||
if sharedFFmpegService != nil {
|
||||
// Process files concurrently using goroutines
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex // Protects processedCount
|
||||
|
||||
for i := range response.Files {
|
||||
fileItem := &response.Files[i]
|
||||
@@ -94,10 +86,10 @@ func processDirectoryMetadata(response *iteminfo.ExtendedFileInfo, idx *indexing
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
// Wait for all goroutines to complete
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// finalizeResponse handles final response adjustments (OnlyOffice ID, scope stripping)
|
||||
@@ -219,16 +211,17 @@ func processContent(info *iteminfo.ExtendedFileInfo, idx *indexing.Index, opts u
|
||||
}
|
||||
|
||||
// Handle subtitles if requested
|
||||
parentPath := filepath.Dir(info.Path)
|
||||
parentInfo, exists := idx.GetMetadataInfo(parentPath, true, false)
|
||||
if exists {
|
||||
info.GetSubtitles(parentInfo)
|
||||
}
|
||||
if opts.ExtractEmbeddedSubtitles {
|
||||
subtitles := ffmpeg.DetectEmbeddedSubtitles(info.RealPath, info.ModTime)
|
||||
info.Subtitles = append(info.Subtitles, subtitles...)
|
||||
parentPath := filepath.Dir(info.Path)
|
||||
parentInfo, exists := idx.GetReducedMetadata(parentPath, true)
|
||||
if exists {
|
||||
info.DetectSubtitles(parentInfo)
|
||||
err := info.LoadSubtitleContent()
|
||||
if err != nil {
|
||||
logger.Debug("failed to load subtitle content: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -243,8 +236,7 @@ func processContent(info *iteminfo.ExtendedFileInfo, idx *indexing.Index, opts u
|
||||
} else {
|
||||
// Copy metadata to ExtendedFileInfo
|
||||
info.Metadata = extItem.Metadata
|
||||
info.HasMetadata = true
|
||||
info.HasPreview = extItem.Metadata != nil && len(extItem.Metadata.AlbumArt) > 0
|
||||
info.HasPreview = extItem.Metadata != nil && extItem.Metadata.AlbumArt != ""
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -331,11 +323,11 @@ func extractAudioMetadata(ctx context.Context, item *iteminfo.ExtendedItemInfo,
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract album art if available (stored as raw bytes, auto-encoded to base64 in JSON)
|
||||
// Extract album art and encode as base64 with strict size limits
|
||||
if picture := m.Picture(); picture != nil && picture.Data != nil {
|
||||
// Size limit to prevent memory issues (max 5MB)
|
||||
// More aggressive size limit to prevent memory issues (max 5MB)
|
||||
if len(picture.Data) <= 5*1024*1024 {
|
||||
item.Metadata.AlbumArt = picture.Data
|
||||
item.Metadata.AlbumArt = base64.StdEncoding.EncodeToString(picture.Data)
|
||||
} else {
|
||||
logger.Debugf("Skipping album art for %s: too large (%d bytes)", realPath, len(picture.Data))
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ func VerifyTotpCode(user *users.User, code string, userStore *users.Storage) err
|
||||
logger.Warningf("Invalid TOTP code for user %s", user.Username)
|
||||
return fmt.Errorf("invalid OTP token")
|
||||
}
|
||||
if totpSecret != "" || !user.OtpEnabled {
|
||||
if totpSecret != "" {
|
||||
user.TOTPSecret = totpSecret // The encrypted or plaintext secret
|
||||
user.TOTPNonce = totpNonce // The nonce if encrypted, or empty if plaintext
|
||||
user.OtpEnabled = true // Enable OTP for the user
|
||||
|
||||
+3
-10
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
|
||||
fbhttp "github.com/gtsteffaniak/filebrowser/backend/http"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/icons"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/preview"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
@@ -182,7 +181,7 @@ func rootCMD(ctx context.Context, store *bolt.BoltStore, serverConfig *settings.
|
||||
cacheDir := settings.Config.Server.CacheDir
|
||||
numWorkers := settings.Config.Server.NumImageProcessors
|
||||
ffmpeg.SetFFmpegPaths()
|
||||
|
||||
|
||||
// Initialize asset filesystem before starting services
|
||||
if settings.Env.EmbeddedFs {
|
||||
embeddedAssets := fbhttp.GetEmbeddedAssets()
|
||||
@@ -194,20 +193,14 @@ func rootCMD(ctx context.Context, store *bolt.BoltStore, serverConfig *settings.
|
||||
} else {
|
||||
fileutils.InitAssetFS(nil, false)
|
||||
}
|
||||
|
||||
// Start preview service
|
||||
|
||||
// setup disk cache
|
||||
err := preview.StartPreviewGenerator(numWorkers, cacheDir)
|
||||
if err != nil {
|
||||
logger.Fatalf("Error starting preview service: %v", err)
|
||||
}
|
||||
logger.Debugf("MuPDF Enabled : %v", settings.Env.MuPdfAvailable)
|
||||
logger.Debugf("Media Enabled : %v", settings.MediaEnabled())
|
||||
|
||||
// Generate PWA icons after preview service is initialized
|
||||
if err := icons.GeneratePWAIcons(); err != nil {
|
||||
logger.Warningf("Failed to generate PWA icons: %v", err)
|
||||
}
|
||||
|
||||
fbhttp.StartHttp(ctx, store, shutdownComplete)
|
||||
return nil
|
||||
}
|
||||
+1
-1
@@ -119,7 +119,7 @@ func updateShowFirstLogin(user *users.User) bool {
|
||||
|
||||
// func to convert legacy user with perm key to permissions
|
||||
func updatePermissions(user *users.User) bool {
|
||||
if user.Version >= 1 {
|
||||
if user.Version == 1 {
|
||||
return false
|
||||
}
|
||||
updateUser := true
|
||||
|
||||
@@ -278,6 +278,8 @@ func setupFrontend(generate bool) {
|
||||
}
|
||||
// Load custom favicon if configured
|
||||
loadCustomFavicon()
|
||||
// Generate PWA icons from custom favicon if needed
|
||||
generatePWAIcons()
|
||||
}
|
||||
|
||||
func setupMedia() {
|
||||
@@ -634,9 +636,14 @@ func setDefaults(generate bool) Settings {
|
||||
database := os.Getenv("FILEBROWSER_DATABASE")
|
||||
if database == "" {
|
||||
database = "database.db"
|
||||
} else {
|
||||
// check if database file exists
|
||||
if _, err := os.Stat(database); os.IsNotExist(err) {
|
||||
database = "database.db"
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(database); os.IsNotExist(err) {
|
||||
logger.Warning("database file could not be found. If this is unexpected, please set the FILEBROWSER_DATABASE environment variable to the correct path.")
|
||||
logger.Warning("database file could not be found. If this is unexpected, the default path is `./database.db`, but it can be configured in the config file under `server.database`.")
|
||||
}
|
||||
s := Settings{
|
||||
Server: Server{
|
||||
@@ -781,6 +788,23 @@ func loadCustomFavicon() {
|
||||
logger.Infof("Using custom favicon: %s", Env.FaviconPath)
|
||||
}
|
||||
|
||||
// generatePWAIcons sets up PWA icon paths (actual generation happens after preview service starts)
|
||||
func generatePWAIcons() {
|
||||
// Set PWA icons directory in cache
|
||||
Env.PWAIconsDir = filepath.Join(Config.Server.CacheDir, "icons")
|
||||
|
||||
// Initialize with default paths (all icons will be generated)
|
||||
Env.PWAIcon192 = "icons/pwa-icon-192.png"
|
||||
Env.PWAIcon256 = "icons/pwa-icon-256.png"
|
||||
Env.PWAIcon512 = "icons/pwa-icon-512.png"
|
||||
|
||||
// Create icons directory with configured permissions
|
||||
// Note: Parent cache directory should already exist from testCacheDirSpeed()
|
||||
if err := os.MkdirAll(Env.PWAIconsDir, fileutils.PermDir); err != nil {
|
||||
logger.Warningf("Failed to create PWA icons directory %s: %v", Env.PWAIconsDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadLoginIcon() {
|
||||
const imageName = "login icon"
|
||||
allowedFormats := []string{".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico"}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -112,93 +111,74 @@ func CollectCommentsAndSecrets(srcPath string) (CommentsMap, SecretFieldsMap, De
|
||||
return comments, secrets, deprecated, nil
|
||||
}
|
||||
|
||||
// parseDirectoryComments parses a single directory for comments, secrets, and deprecated fields.
|
||||
// parseDirectoryComments parses a single directory for comments, secrets, and deprecated fields
|
||||
func parseDirectoryComments(dir string) (CommentsMap, SecretFieldsMap, DeprecatedFieldsMap, error) {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
fset := token.NewFileSet()
|
||||
pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
var files []*ast.File
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedSyntax | packages.NeedFiles,
|
||||
Dir: absDir,
|
||||
ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
|
||||
return parser.ParseFile(fset, filename, src, parser.ParseComments)
|
||||
},
|
||||
}
|
||||
pkgs, err := packages.Load(cfg, ".")
|
||||
if err == nil && len(pkgs) > 0 {
|
||||
for _, pkg := range pkgs {
|
||||
files = append(files, pkg.Syntax...)
|
||||
}
|
||||
}
|
||||
if len(files) == 0 {
|
||||
// Fallback for directories outside a module (e.g. test temp dirs): parse .go files directly
|
||||
files, err = parseDirGoFiles(absDir)
|
||||
if err != nil || len(files) == 0 {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
comments := make(CommentsMap)
|
||||
secrets := make(SecretFieldsMap)
|
||||
deprecated := make(DeprecatedFieldsMap)
|
||||
|
||||
for _, file := range files {
|
||||
for _, decl := range file.Decls {
|
||||
gen, ok := decl.(*ast.GenDecl)
|
||||
if !ok || gen.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
for _, spec := range gen.Specs {
|
||||
ts, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
for _, pkg := range pkgs {
|
||||
for _, file := range pkg.Files {
|
||||
for _, decl := range file.Decls {
|
||||
gen, ok := decl.(*ast.GenDecl)
|
||||
if !ok || gen.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
st, ok := ts.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
for _, spec := range gen.Specs {
|
||||
ts, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name := field.Names[0].Name
|
||||
var parts []string
|
||||
var fullComment string
|
||||
|
||||
if field.Doc != nil {
|
||||
docText := strings.TrimSpace(field.Doc.Text())
|
||||
parts = append(parts, docText)
|
||||
fullComment += docText + " "
|
||||
}
|
||||
if field.Comment != nil {
|
||||
commentText := strings.TrimSpace(field.Comment.Text())
|
||||
parts = append(parts, commentText)
|
||||
fullComment += commentText
|
||||
st, ok := ts.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
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
|
||||
|
||||
if len(parts) > 0 {
|
||||
commentMap[name] = strings.Join(parts, " : ")
|
||||
}
|
||||
for _, field := range st.Fields.List {
|
||||
if len(field.Names) == 0 {
|
||||
continue
|
||||
}
|
||||
name := field.Names[0].Name
|
||||
var parts []string
|
||||
var fullComment string
|
||||
|
||||
// Check if field should be treated as secret
|
||||
if strings.Contains(strings.ToLower(fullComment), "secret:") {
|
||||
secretMap[name] = true
|
||||
}
|
||||
if field.Doc != nil {
|
||||
docText := strings.TrimSpace(field.Doc.Text())
|
||||
parts = append(parts, docText)
|
||||
fullComment += docText + " "
|
||||
}
|
||||
if field.Comment != nil {
|
||||
commentText := strings.TrimSpace(field.Comment.Text())
|
||||
parts = append(parts, commentText)
|
||||
fullComment += commentText
|
||||
}
|
||||
|
||||
// Check if field should be treated as deprecated
|
||||
if strings.Contains(strings.ToLower(fullComment), "deprecated:") {
|
||||
deprecatedMap[name] = true
|
||||
if len(parts) > 0 {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,29 +187,6 @@ func parseDirectoryComments(dir string) (CommentsMap, SecretFieldsMap, Deprecate
|
||||
return comments, secrets, deprecated, nil
|
||||
}
|
||||
|
||||
// parseDirGoFiles parses all .go files in dir and returns their ASTs. Used when the directory
|
||||
// is not inside a Go module (e.g. test temp dirs) so packages.Load would fail.
|
||||
func parseDirGoFiles(dir string) ([]*ast.File, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
var files []*ast.File
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, e.Name())
|
||||
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// PathToTypeField represents a mapping from YAML path to Go type and field info
|
||||
type PathToTypeField struct {
|
||||
TypeName string
|
||||
|
||||
@@ -63,18 +63,12 @@ type FileOptions struct {
|
||||
// SanitizeUserPath prevents path traversal attacks by cleaning and validating user input.
|
||||
// Rule 1: Do Not Use User Input in File Paths (without validation)
|
||||
func SanitizeUserPath(userPath string) (string, error) {
|
||||
// Clean resolves . and .. segments
|
||||
clean := filepath.Clean(userPath)
|
||||
|
||||
// Split the path into segments to check for path traversal attempts
|
||||
// We check if ".." appears as a complete path segment (not just in a filename)
|
||||
segments := strings.Split(clean, string(filepath.Separator))
|
||||
|
||||
for _, segment := range segments {
|
||||
// Check if any segment is exactly ".." (path traversal attempt)
|
||||
// This catches any ".." that filepath.Clean couldn't resolve (i.e., escape attempts)
|
||||
if segment == ".." {
|
||||
return "", fmt.Errorf("invalid path: path traversal detected")
|
||||
}
|
||||
// Reject absolute paths or attempts to escape scope
|
||||
if strings.HasPrefix(clean, "..") {
|
||||
return "", fmt.Errorf("invalid path")
|
||||
}
|
||||
|
||||
return clean, nil
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
type SubtitleTrack struct {
|
||||
Name string `json:"name"` // filename for external, or descriptive name for embedded
|
||||
Language string `json:"language"` // 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
|
||||
Embedded bool `json:"embedded"` // true for embedded subtitles, false for external files
|
||||
}
|
||||
|
||||
func GetSubtitleSidecarContent(subtitlePath string) (string, error) {
|
||||
// validate the size is not too large
|
||||
info, err := os.Stat(subtitlePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.Size() > 1024*1024*50 { // 50MB
|
||||
return "", errors.New("subtitle file is too large")
|
||||
}
|
||||
|
||||
// First check if it's a text file using the consolidated validation logic
|
||||
isText, err := IsTextFile(subtitlePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !isText {
|
||||
// Not a text file, return empty string (no error, just not text)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// read the file content
|
||||
content, err := os.ReadFile(subtitlePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
|
||||
// IsSingleFileShare determines if this share is for a single file (not a directory).
|
||||
@@ -145,28 +144,10 @@ func (l *Link) HasReachedUserLimit(username string) bool {
|
||||
return count >= l.DownloadsLimit
|
||||
}
|
||||
|
||||
func (l *Link) GetSourceName() string {
|
||||
func (l *Link) GetSourceName() (string, error) {
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[l.Source]
|
||||
if !ok {
|
||||
return ""
|
||||
return "", fmt.Errorf("source not found")
|
||||
}
|
||||
return sourceInfo.Name
|
||||
}
|
||||
|
||||
func (l *Link) UserCanEdit(user *users.User) bool {
|
||||
return l.UserID == user.ID || user.Permissions.Admin
|
||||
}
|
||||
|
||||
func (l *Link) SourceURL(user *users.User) string {
|
||||
sourceName := l.GetSourceName()
|
||||
// get user scope path from share
|
||||
userScope, err := user.GetScopeForSourceName(sourceName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(l.Path, userScope) {
|
||||
return ""
|
||||
}
|
||||
scopedPath := strings.TrimPrefix(l.Path, userScope)
|
||||
return "/" + filepath.Join("files", sourceName, scopedPath)
|
||||
return sourceInfo.Name, nil
|
||||
}
|
||||
@@ -43,10 +43,8 @@ type CommonShare struct {
|
||||
AllowReplacements bool `json:"allowReplacements,omitempty"` // allow replacements of files
|
||||
SidebarLinks []users.SidebarLink `json:"sidebarLinks"` // customizable sidebar links
|
||||
HasPassword bool `json:"hasPassword,omitempty"`
|
||||
ShowHidden bool `json:"showHidden,omitempty"` // show hidden files in share (true = show, false = hide)
|
||||
DisableLoginOption bool `json:"disableLoginOption"` // disable login option in share (true = hide, false = show)
|
||||
SourceURL string `json:"sourceURL,omitempty"`
|
||||
CanEdit bool `json:"canEdit,omitempty"`
|
||||
ShowHidden bool `json:"showHidden,omitempty"` // show hidden files in share (true = show, false = hide)
|
||||
DisableLoginOption bool `json:"disableLoginOption,omitempty"` // disable login option in share (true = hide, false = show)
|
||||
}
|
||||
type CreateBody struct {
|
||||
CommonShare
|
||||
|
||||
@@ -247,6 +247,7 @@ func (s *Storage) UpdateShares(oldSource, oldPath, newSource, newPath string) (i
|
||||
continue
|
||||
}
|
||||
|
||||
oldFullPath := l.Path
|
||||
l.Source = newSource
|
||||
l.Path = newPath
|
||||
|
||||
@@ -263,8 +264,14 @@ func (s *Storage) UpdateShares(oldSource, oldPath, newSource, newPath string) (i
|
||||
s.shareCache[l.Hash] = l
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
logger.Info("share updated", "hash", l.Hash, "fromPath", oldFullPath, "toPath", l.Path)
|
||||
updated++
|
||||
}
|
||||
|
||||
if updated == 0 {
|
||||
logger.Warning("no matching shares to update for provided source/path")
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ func (db *IndexDB) ExplainQueryPlan(query string, args ...interface{}) (string,
|
||||
if err := rows.Scan(&id, &parent, ¬used, &detail); err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintf(&plan, "%d|%d|%s\n", id, parent, detail)
|
||||
plan.WriteString(fmt.Sprintf("%d|%d|%s\n", id, parent, detail))
|
||||
}
|
||||
return plan.String(), rows.Err()
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...str
|
||||
|
||||
passwordUser := existingUser.LoginMethod == users.LoginMethodPassword
|
||||
enforcedOtp := settings.Config.Auth.Methods.PasswordAuth.EnforcedOtp
|
||||
if passwordUser && enforcedOtp && !existingUser.OtpEnabled {
|
||||
if passwordUser && enforcedOtp && !user.OtpEnabled {
|
||||
return errors.ErrNoTotpConfigured
|
||||
}
|
||||
if user.LoginMethod == "" {
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
var userStore *users.Storage
|
||||
|
||||
func InitializeDb(path string) (*bolt.BoltStore, bool, error) {
|
||||
logger.Debugf("Initializing database: %v", path)
|
||||
exists, err := dbExists(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -3,12 +3,11 @@ package ffmpeg
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
)
|
||||
|
||||
var (
|
||||
MediaCache = cache.NewCache[[]utils.SubtitleTrack](24 * time.Hour) // subtitles get cached for 24 hours
|
||||
SubtitleContentCache = cache.NewCache[string](24 * time.Hour) // subtitle content gets cached for 24 hours
|
||||
MetadataCache = cache.NewCache[float64](24 * time.Hour) // media duration gets cached for 24 hours
|
||||
MediaCache = cache.NewCache[[]SubtitleTrack](24 * time.Hour) // subtitles get cached for 24 hours
|
||||
SubtitleContentCache = cache.NewCache[string](24 * time.Hour) // subtitle content gets cached for 24 hours
|
||||
MetadataCache = cache.NewCache[float64](24 * time.Hour) // media duration gets cached for 24 hours
|
||||
)
|
||||
+136
-33
@@ -10,10 +10,20 @@ import (
|
||||
"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 {
|
||||
@@ -26,7 +36,7 @@ type FFProbeOutput struct {
|
||||
}
|
||||
|
||||
// DetectAllSubtitles finds both embedded and external subtitle tracks
|
||||
func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) []utils.SubtitleTrack {
|
||||
func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) []SubtitleTrack {
|
||||
key := "all_subtitles:" + videoPath + ":" + modtime.Format(time.RFC3339)
|
||||
|
||||
// Check cache first
|
||||
@@ -34,7 +44,7 @@ func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) [
|
||||
return cached
|
||||
}
|
||||
|
||||
var allSubtitles []utils.SubtitleTrack
|
||||
var allSubtitles []SubtitleTrack
|
||||
|
||||
// First, get embedded subtitles
|
||||
embeddedSubs := detectEmbeddedSubtitles(videoPath)
|
||||
@@ -50,25 +60,9 @@ func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) [
|
||||
return allSubtitles
|
||||
}
|
||||
|
||||
// DetectEmbeddedSubtitles detects embedded subtitle streams using ffprobe.
|
||||
// This is the public API that can be called from other packages.
|
||||
// Returns empty array if ffprobe is not available or fails.
|
||||
func DetectEmbeddedSubtitles(videoPath string, modtime time.Time) []utils.SubtitleTrack {
|
||||
// Check cache first
|
||||
key := "embedded_subtitles:" + videoPath + ":" + modtime.Format(time.RFC3339)
|
||||
if cached, ok := MediaCache.Get(key); ok {
|
||||
return cached
|
||||
}
|
||||
// Detect embedded subtitles
|
||||
subtitles := detectEmbeddedSubtitles(videoPath)
|
||||
// Cache the results
|
||||
MediaCache.Set(key, subtitles)
|
||||
return subtitles
|
||||
}
|
||||
|
||||
// detectEmbeddedSubtitles uses ffprobe to find embedded subtitle tracks
|
||||
// Always runs ffprobe - results are cached for performance
|
||||
func detectEmbeddedSubtitles(realPath string) []utils.SubtitleTrack {
|
||||
func detectEmbeddedSubtitles(realPath string) []SubtitleTrack {
|
||||
cmd := exec.Command("ffprobe",
|
||||
"-v", "quiet",
|
||||
"-print_format", "json",
|
||||
@@ -88,13 +82,13 @@ func detectEmbeddedSubtitles(realPath string) []utils.SubtitleTrack {
|
||||
return nil
|
||||
}
|
||||
|
||||
var subtitles []utils.SubtitleTrack
|
||||
var subtitles []SubtitleTrack
|
||||
for _, stream := range probeOutput.Streams {
|
||||
if stream.CodecType == "subtitle" {
|
||||
track := utils.SubtitleTrack{
|
||||
Index: &stream.Index,
|
||||
Codec: stream.CodecName,
|
||||
Embedded: true, // This is an embedded subtitle
|
||||
track := SubtitleTrack{
|
||||
Index: &stream.Index,
|
||||
Codec: stream.CodecName,
|
||||
IsFile: false, // This is an embedded subtitle
|
||||
}
|
||||
|
||||
// Extract language and title from tags
|
||||
@@ -123,8 +117,8 @@ func detectEmbeddedSubtitles(realPath string) []utils.SubtitleTrack {
|
||||
}
|
||||
|
||||
// detectExternalSubtitles finds external subtitle files in the same directory
|
||||
func detectExternalSubtitles(videoPath string, parentDir string) []utils.SubtitleTrack {
|
||||
var subtitles []utils.SubtitleTrack
|
||||
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))
|
||||
@@ -137,9 +131,9 @@ func detectExternalSubtitles(videoPath string, parentDir string) []utils.Subtitl
|
||||
subtitlePath := filepath.Join(parentDir, videoBaseName+ext)
|
||||
if _, err := os.Stat(subtitlePath); err == nil {
|
||||
// File exists
|
||||
track := utils.SubtitleTrack{
|
||||
Name: filepath.Base(subtitlePath),
|
||||
Embedded: false, // This is an external file
|
||||
track := SubtitleTrack{
|
||||
Name: filepath.Base(subtitlePath),
|
||||
IsFile: true, // This is an external file
|
||||
}
|
||||
|
||||
// Try to infer language from filename patterns like "video.en.srt"
|
||||
@@ -176,11 +170,120 @@ func ExtractSubtitleContent(videoPath string, streamIndex int) (string, error) {
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// LoadSubtitleFile loads a subtitle file and returns its raw content
|
||||
func LoadSubtitleFile(subtitlePath string) (string, error) {
|
||||
// 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
|
||||
}
|
||||
return string(content), nil
|
||||
|
||||
// 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 := SubtitleContentCache.Get(contentKey); 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
|
||||
SubtitleContentCache.Set(contentKey, content)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+46
-46
@@ -15,23 +15,22 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gtsteffaniak/go-cache v1.1.0
|
||||
github.com/gtsteffaniak/go-logger v1.0.2
|
||||
github.com/kovidgoyal/imaging v1.8.20
|
||||
github.com/mattn/go-sqlite3 v1.14.34
|
||||
github.com/kovidgoyal/imaging v1.8.19
|
||||
github.com/mattn/go-sqlite3 v1.14.33
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/image v0.36.0
|
||||
golang.org/x/mod v0.33.0
|
||||
golang.org/x/net v0.51.0
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/image v0.35.0
|
||||
golang.org/x/mod v0.32.0
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/time v0.14.0
|
||||
golang.org/x/tools v0.42.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.46.1
|
||||
modernc.org/sqlite v1.44.3
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -53,17 +52,16 @@ require (
|
||||
github.com/Djarvur/go-err113 v0.1.1 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/MirrexOne/unqueryvet v1.5.3 // indirect
|
||||
github.com/MirrexOne/unqueryvet v1.4.0 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||
github.com/air-verse/air v1.64.5 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
|
||||
github.com/air-verse/air v1.64.2 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.21.1 // 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.2 // indirect
|
||||
github.com/alexkohler/prealloc v1.0.1 // 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/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect
|
||||
github.com/ashanbrown/makezero/v2 v2.1.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
@@ -73,7 +71,7 @@ require (
|
||||
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.6.0 // indirect
|
||||
github.com/bombsimon/wsl/v5 v5.3.0 // 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
|
||||
@@ -85,7 +83,7 @@ require (
|
||||
github.com/charithe/durationcheck v0.0.11 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
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/ckaznocha/intrange v0.3.1 // indirect
|
||||
@@ -99,7 +97,7 @@ require (
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/ettle/strcase v0.2.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
@@ -107,20 +105,20 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/ghostiam/protogetter v0.3.20 // indirect
|
||||
github.com/ghostiam/protogetter v0.3.18 // indirect
|
||||
github.com/go-critic/go-critic v0.14.3 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.5 // indirect
|
||||
github.com/go-openapi/spec v0.22.4 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/spec v0.22.3 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/go-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
|
||||
@@ -130,21 +128,21 @@ 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.5.0 // 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/godoc-lint/godoc-lint v0.11.2 // indirect
|
||||
github.com/godoc-lint/godoc-lint v0.11.1 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/gohugoio/hugo v0.149.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a // indirect
|
||||
github.com/golang/geo v0.0.0-20260129164528-943061e2742c // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golangci/asciicheck v0.5.0 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.1 // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
|
||||
github.com/golangci/golangci-lint/v2 v2.10.1 // indirect
|
||||
github.com/golangci/golines v0.15.0 // indirect
|
||||
github.com/golangci/misspell v0.8.0 // indirect
|
||||
github.com/golangci/golangci-lint/v2 v2.8.0 // indirect
|
||||
github.com/golangci/golines v0.14.0 // 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
|
||||
@@ -166,7 +164,7 @@ require (
|
||||
github.com/jjti/go-spancheck v0.6.5 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/julz/importas v0.2.0 // indirect
|
||||
github.com/jupiterrider/ffi v0.6.0 // indirect
|
||||
github.com/jupiterrider/ffi v0.5.1 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
|
||||
github.com/kisielk/errcheck v1.9.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||
@@ -196,7 +194,7 @@ 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.14.0 // indirect
|
||||
github.com/mgechev/revive v1.13.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
||||
github.com/moricho/tparallel v0.3.2 // indirect
|
||||
@@ -205,7 +203,7 @@ require (
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nishanths/exhaustive v0.12.0 // indirect
|
||||
github.com/nishanths/predeclared v0.2.2 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.23.0 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.21.2 // 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
|
||||
@@ -230,8 +228,8 @@ require (
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.23.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/securego/gosec/v2 v2.22.11 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||
github.com/sonatard/noctx v0.4.0 // indirect
|
||||
github.com/sourcegraph/go-diff v0.7.0 // indirect
|
||||
@@ -264,21 +262,23 @@ require (
|
||||
gitlab.com/bosi/decorder v0.4.2 // indirect
|
||||
go-simpler.org/musttag v0.14.0 // indirect
|
||||
go-simpler.org/sloglint v0.11.1 // indirect
|
||||
go.augendre.info/arangolint v0.4.0 // indirect
|
||||
go.augendre.info/arangolint v0.3.1 // indirect
|
||||
go.augendre.info/fatcontext v0.9.0 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // 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.7.0 // indirect
|
||||
modernc.org/libc v1.69.0 // indirect
|
||||
honnef.co/go/tools v0.6.1 // indirect
|
||||
modernc.org/libc v1.67.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
mvdan.cc/gofumpt v0.9.2 // indirect
|
||||
|
||||
+115
-112
@@ -73,18 +73,18 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/MirrexOne/unqueryvet v1.5.3 h1:LpT3rsH+IY3cQddWF9bg4C7jsbASdGnrOSofY8IPEiw=
|
||||
github.com/MirrexOne/unqueryvet v1.5.3/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
|
||||
github.com/MirrexOne/unqueryvet v1.4.0 h1:6KAkqqW2KUnkl9Z0VuTphC3IXRPoFqEkJEtyxxHj5eQ=
|
||||
github.com/MirrexOne/unqueryvet v1.4.0/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/air-verse/air v1.64.5 h1:+gs/NgTzYYe+gGPyfHy3XxpJReQWC1pIsiKIg0LgNt4=
|
||||
github.com/air-verse/air v1.64.5/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY=
|
||||
github.com/air-verse/air v1.64.2 h1:H49xKRa4r3uFUycmCCA+JectVuZLNMPGdypLFpoHVcY=
|
||||
github.com/air-verse/air v1.64.2/go.mod h1:SOwv08NTvKextanAY819AK0FK6XzQO5/tC6s1R+rk8o=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA=
|
||||
github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
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.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
@@ -96,16 +96,14 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
|
||||
github.com/alexkohler/prealloc v1.0.2 h1:MPo8cIkGkZytq7WNH9UHv3DIX1mPz1RatPXnZb0zHWQ=
|
||||
github.com/alexkohler/prealloc v1.0.2/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
|
||||
github.com/alexkohler/prealloc v1.0.1 h1:A9P1haqowqUxWvU9nk6tQ7YktXIHf+LQM9wPRhuteEE=
|
||||
github.com/alexkohler/prealloc v1.0.1/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
|
||||
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/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
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=
|
||||
@@ -156,8 +154,8 @@ github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ
|
||||
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.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8=
|
||||
github.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU=
|
||||
github.com/bombsimon/wsl/v5 v5.3.0 h1:nZWREJFL6U3vgW/B1lfDOigl+tEF6qgs6dGGbFeR0UM=
|
||||
github.com/bombsimon/wsl/v5 v5.3.0/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=
|
||||
@@ -184,8 +182,8 @@ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4p
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
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=
|
||||
@@ -244,8 +242,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -275,8 +273,8 @@ github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz
|
||||
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.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=
|
||||
github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
|
||||
github.com/ghostiam/protogetter v0.3.18 h1:yEpghRGtP9PjKvVXtEzGpYfQj1Wl/ZehAfU6fr62Lfo=
|
||||
github.com/ghostiam/protogetter v0.3.18/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
|
||||
github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
|
||||
github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
@@ -298,33 +296,33 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
|
||||
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
|
||||
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
||||
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
|
||||
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
|
||||
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
|
||||
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
|
||||
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
|
||||
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
|
||||
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
|
||||
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
|
||||
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
|
||||
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
|
||||
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
|
||||
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
|
||||
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
|
||||
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
|
||||
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-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=
|
||||
@@ -357,8 +355,8 @@ 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.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/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=
|
||||
@@ -367,8 +365,8 @@ 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.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM=
|
||||
github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo=
|
||||
github.com/godoc-lint/godoc-lint v0.11.1 h1:z9as8Qjiy6miRIa3VRymTa+Gt2RLnGICVikcvlUVOaA=
|
||||
github.com/godoc-lint/godoc-lint v0.11.1/go.mod h1:BAqayheFSuZrEAqCRxgw9MyvsM+S/hZwJbU1s/ejRj8=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -393,8 +391,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a h1:7f/cr+n7Jtd7mzCLrx47sdVIGabpSSxCHrf6fJczDp0=
|
||||
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
||||
github.com/golang/geo v0.0.0-20260129164528-943061e2742c h1:ysO2h2Odnl1AJM1I2Lm/fa6JvO0pECMSt2CwBaa+ITo=
|
||||
github.com/golang/geo v0.0.0-20260129164528-943061e2742c/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
||||
github.com/golang/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=
|
||||
@@ -434,12 +432,12 @@ github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarog
|
||||
github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
|
||||
github.com/golangci/golangci-lint/v2 v2.10.1 h1:flhw5Px6ojbLyEFzXvJn5B2HEdkkRlkhE1SnmCbQBiE=
|
||||
github.com/golangci/golangci-lint/v2 v2.10.1/go.mod h1:dBsrOk6zj0vDhlTv+IiJGqkDokR24IVTS7W3EVfPTQY=
|
||||
github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
|
||||
github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
|
||||
github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=
|
||||
github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
|
||||
github.com/golangci/golangci-lint/v2 v2.8.0 h1:wJnr3hJWY3eVzOUcfwbDc2qbi2RDEpvLmQeNFaPSNYA=
|
||||
github.com/golangci/golangci-lint/v2 v2.8.0/go.mod h1:xl+HafQ9xoP8rzw0z5AwnO5kynxtb80e8u02Ej/47RI=
|
||||
github.com/golangci/golines v0.14.0 h1:xt9d3RKBjhasA3qpoXs99J2xN2t6eBlpLHt0TrgyyXc=
|
||||
github.com/golangci/golines v0.14.0/go.mod h1:gf555vPG2Ia7mmy2mzmhVQbVjuK8Orw0maR1G4vVAAQ=
|
||||
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=
|
||||
@@ -473,8 +471,8 @@ 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-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -545,8 +543,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
|
||||
github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
|
||||
github.com/jupiterrider/ffi v0.6.0 h1:UX378KcZvH5c8qgLi9KL/bL82SZTHdRspZ+jj7bvBng=
|
||||
github.com/jupiterrider/ffi v0.6.0/go.mod h1:PqZ5Go6X9by8CIXgfprxfMPYmn8oT5m2O7AA56s64bY=
|
||||
github.com/jupiterrider/ffi v0.5.1 h1:l7ANXU+Ex33LilVa283HNaf/sTzCrrht7D05k6T6nlc=
|
||||
github.com/jupiterrider/ffi v0.5.1/go.mod h1:x7xdNKo8h0AmLuXfswDUBxUsd2OqUP4ekC8sCnsmbvo=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
|
||||
github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M=
|
||||
@@ -560,8 +558,8 @@ github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811ui
|
||||
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
|
||||
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
|
||||
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
|
||||
github.com/kovidgoyal/imaging v1.8.20 h1:74GZ7C2rIm3rqmGEjK1GvvPOOnJ0SS5iDOa6Flfo0b0=
|
||||
github.com/kovidgoyal/imaging v1.8.20/go.mod h1:d3phGYkTChGYkY4y++IjpHgUGhWGELDc2NEQAqxwZZg=
|
||||
github.com/kovidgoyal/imaging v1.8.19 h1:zWJdQqF2tfSKjvoB7XpLRhVGbYsze++M0iaqZ4ZkhNk=
|
||||
github.com/kovidgoyal/imaging v1.8.19/go.mod h1:I0q8RdoEuyc4G8GFOF9CaluTUHQSf68d6TmsqpvfRI8=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -624,12 +622,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgechev/revive v1.14.0 h1:CC2Ulb3kV7JFYt+izwORoS3VT/+Plb8BvslI/l1yZsc=
|
||||
github.com/mgechev/revive v1.14.0/go.mod h1:MvnujelCZBZCaoDv5B3foPo6WWgULSSFxvfxp7GsPfo=
|
||||
github.com/mgechev/revive v1.13.0 h1:yFbEVliCVKRXY8UgwEO7EOYNopvjb1BFbmYqm9hZjBM=
|
||||
github.com/mgechev/revive v1.13.0/go.mod h1:efJfeBVCX2JUumNQ7dtOLDja+QKj9mYGgEZA7rt5u+0=
|
||||
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=
|
||||
@@ -661,8 +659,8 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK
|
||||
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.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8=
|
||||
github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4=
|
||||
github.com/nunnatsa/ginkgolinter v0.21.2 h1:khzWfm2/Br8ZemX8QM1pl72LwM+rMeW6VUbQ4rzh0Po=
|
||||
github.com/nunnatsa/ginkgolinter v0.21.2/go.mod h1:GItSI5fw7mCGLPmkvGYrr1kEetZe7B593jcyOpyabsY=
|
||||
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=
|
||||
@@ -673,10 +671,10 @@ 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.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
||||
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
|
||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
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=
|
||||
@@ -700,6 +698,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
@@ -759,8 +759,8 @@ github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tM
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
|
||||
github.com/securego/gosec/v2 v2.23.0 h1:h4TtF64qFzvnkqvsHC/knT7YC5fqyOCItlVR8+ptEBo=
|
||||
github.com/securego/gosec/v2 v2.23.0/go.mod h1:qRHEgXLFuYUDkI2T7W7NJAmOkxVhkR0x9xyHOIcMNZ0=
|
||||
github.com/securego/gosec/v2 v2.22.11 h1:tW+weM/hCM/GX3iaCV91d5I6hqaRT2TPsFM1+USPXwg=
|
||||
github.com/securego/gosec/v2 v2.22.11/go.mod h1:KE4MW/eH0GLWztkbt4/7XpyH0zJBBnu7sYB4l6Wn7Mw=
|
||||
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=
|
||||
@@ -769,8 +769,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
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.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o=
|
||||
@@ -802,6 +802,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
||||
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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=
|
||||
@@ -852,8 +853,6 @@ github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pH
|
||||
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
|
||||
github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
|
||||
github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
|
||||
@@ -879,8 +878,8 @@ go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
|
||||
go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
|
||||
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.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
|
||||
go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
|
||||
go.augendre.info/arangolint v0.3.1 h1:n2E6p8f+zfXSFLa2e2WqFPp4bfvcuRdd50y6cT65pSo=
|
||||
go.augendre.info/arangolint v0.3.1/go.mod h1:6ZKzEzIZuBQwoSvlKT+qpUfIbBfFCE5gbAoTg0/117g=
|
||||
go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE=
|
||||
go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
@@ -891,6 +890,8 @@ 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/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.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=
|
||||
@@ -908,8 +909,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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
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=
|
||||
@@ -920,16 +921,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-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 h1:HDjDiATsGqvuqvkDvgJjD1IgPrVekcSXVVE21JwvzGE=
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
||||
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.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||
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=
|
||||
@@ -955,8 +956,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.8.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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
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=
|
||||
@@ -999,16 +1000,16 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
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.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.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=
|
||||
@@ -1070,6 +1071,7 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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-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=
|
||||
@@ -1078,8 +1080,8 @@ 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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -1096,8 +1098,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
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=
|
||||
@@ -1153,8 +1155,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
@@ -1260,6 +1262,7 @@ gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1269,22 +1272,22 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
|
||||
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
|
||||
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
||||
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.31.0 h1:/bsaxqdgX3gy/0DboxcvWrc3NpzH+6wpFfI/ZaA/hrg=
|
||||
modernc.org/ccgo/v4 v4.31.0/go.mod h1:jKe8kPBjIN/VdGTVqARTQ8N1gAziBmiISY8j5HoKwjg=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.69.0 h1:YQJ5QMSReTgQ3QFmI0dudfjXIjCcYTUxcH8/9P9f0D8=
|
||||
modernc.org/libc v1.69.0/go.mod h1:YfLLduUEbodNV2xLU5JOnRHBTAHVHsVW3bVYGw0ZCV4=
|
||||
modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=
|
||||
modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -1293,8 +1296,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
||||
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
libError "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -397,6 +398,10 @@ func authenticateShareRequest(r *http.Request, l *share.Link) (int, error) {
|
||||
}
|
||||
|
||||
password := r.Header.Get("X-SHARE-PASSWORD")
|
||||
password, err := url.QueryUnescape(password)
|
||||
if err != nil {
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(l.PasswordHash), []byte(password)); err != nil {
|
||||
if libError.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return http.StatusUnauthorized, nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -555,10 +556,7 @@ func mergeGroupsByChecksum(groups []duplicateGroupWithChecksums) []duplicateGrou
|
||||
|
||||
func prepDuplicatesOptions(r *http.Request, d *requestContext) (*duplicatesOptions, error) {
|
||||
source := r.URL.Query().Get("source")
|
||||
scope, err := utils.SanitizeUserPath(r.URL.Query().Get("scope"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scope: %v", err)
|
||||
}
|
||||
scope := r.URL.Query().Get("scope")
|
||||
|
||||
minSizeMbStr := r.URL.Query().Get("minSizeMb")
|
||||
// First pass uses filename matching for speed, then checksums verify final groups
|
||||
@@ -567,14 +565,19 @@ func prepDuplicatesOptions(r *http.Request, d *requestContext) (*duplicatesOptio
|
||||
// Default minimum size: 1MB
|
||||
minSizeMb := int64(1)
|
||||
if minSizeMbStr != "" {
|
||||
if _, err = fmt.Sscanf(minSizeMbStr, "%d", &minSizeMb); err != nil {
|
||||
if _, err := fmt.Sscanf(minSizeMbStr, "%d", &minSizeMb); err != nil {
|
||||
return nil, fmt.Errorf("invalid minSizeMb parameter: %w", err)
|
||||
}
|
||||
}
|
||||
// Convert MB to bytes
|
||||
minSize := minSizeMb * 1024 * 1024
|
||||
|
||||
searchScope := strings.TrimPrefix(scope, ".")
|
||||
unencodedScope, err := url.PathUnescape(scope)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
searchScope := strings.TrimPrefix(unencodedScope, ".")
|
||||
|
||||
index := indexing.GetIndex(source)
|
||||
if index == nil {
|
||||
|
||||
@@ -137,12 +137,8 @@ func fileWatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if path == "" || source == "" {
|
||||
return http.StatusBadRequest, fmt.Errorf("path and source are required")
|
||||
}
|
||||
var err error
|
||||
path, err = utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
|
||||
var err error
|
||||
// Parse lines parameter
|
||||
lines := 10 // default
|
||||
if linesStr != "" {
|
||||
@@ -286,13 +282,6 @@ func fileWatchSSEHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
return http.StatusBadRequest, fmt.Errorf("path and source are required")
|
||||
}
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
path = cleanPath
|
||||
|
||||
// Parse lines parameter
|
||||
lines := 10 // default
|
||||
if linesStr != "" {
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
_ "net/http/pprof"
|
||||
@@ -74,9 +74,6 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
|
||||
devMode: settings.Env.IsDevMode,
|
||||
}
|
||||
|
||||
// Initialize cached PWA manifest
|
||||
InitializePWAManifest()
|
||||
|
||||
router := http.NewServeMux()
|
||||
// API group routing
|
||||
api := http.NewServeMux()
|
||||
|
||||
+25
-54
@@ -4,26 +4,24 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
)
|
||||
|
||||
// subtitlesHandler handles subtitle requests for both external files and embedded streams
|
||||
// @Summary Get subtitle content
|
||||
// @Description Returns raw subtitle content from external files or embedded streams
|
||||
// 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/plain
|
||||
// @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 name query string true "Subtitle track name (filename for external, descriptive name for embedded)"
|
||||
// @Param embedded query bool false "Whether this is an embedded stream (true) or external file (false), defaults to false"
|
||||
// @Success 200 {string} string "Raw subtitle content in original format"
|
||||
// @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"
|
||||
@@ -32,11 +30,10 @@ import (
|
||||
func subtitlesHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
path := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
name := r.URL.Query().Get("name")
|
||||
embedded := r.URL.Query().Get("embedded") == "true"
|
||||
indexParam := r.URL.Query().Get("index")
|
||||
|
||||
if name == "" {
|
||||
return http.StatusBadRequest, fmt.Errorf("name parameter is required")
|
||||
if indexParam == "" {
|
||||
indexParam = "0" // default to first subtitle stream
|
||||
}
|
||||
|
||||
userscope, err := d.user.GetScopeForSourceName(source)
|
||||
@@ -48,54 +45,28 @@ func subtitlesHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
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)
|
||||
}
|
||||
|
||||
parentDir := filepath.Dir(realPath)
|
||||
var content string
|
||||
|
||||
if !embedded {
|
||||
content, err = utils.GetSubtitleSidecarContent(filepath.Join(parentDir, name))
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to get subtitle sidecar content: %v", err)
|
||||
}
|
||||
} else {
|
||||
// For embedded subtitles, we need to find the stream index by name
|
||||
// Get file modification time for caching
|
||||
fileInfo, err := os.Stat(realPath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to stat file: %v", err)
|
||||
}
|
||||
|
||||
// Detect embedded subtitles
|
||||
embeddedSubs := ffmpeg.DetectEmbeddedSubtitles(realPath, fileInfo.ModTime())
|
||||
|
||||
// Find the subtitle track by name
|
||||
var streamIndex *int
|
||||
for _, sub := range embeddedSubs {
|
||||
if sub.Name == name {
|
||||
streamIndex = sub.Index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if streamIndex == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("embedded subtitle track '%s' not found", name)
|
||||
}
|
||||
|
||||
content, err = ffmpeg.ExtractSubtitleContent(realPath, *streamIndex)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to extract embedded subtitle: %v", err)
|
||||
}
|
||||
metadata, exists := idx.GetMetadataInfo(userscope, true, false)
|
||||
if !exists {
|
||||
return http.StatusNotFound, fmt.Errorf("file not found: %v", err)
|
||||
}
|
||||
|
||||
// Return raw content with appropriate content type
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
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, name, time.Now(), bytes.NewReader([]byte(content)))
|
||||
http.ServeContent(w, r, fmt.Sprintf("%s-%d.vtt", subtitle.Name, index),
|
||||
time.Now(), bytes.NewReader([]byte(subtitle.Content)))
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
+15
-14
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -25,7 +26,6 @@ import (
|
||||
|
||||
type requestContext struct {
|
||||
user *users.User
|
||||
shareUser *users.User
|
||||
fileInfo iteminfo.ExtendedFileInfo
|
||||
token string
|
||||
share *share.Link
|
||||
@@ -51,7 +51,12 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, data *requestContex
|
||||
func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
return withOrWithoutUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
|
||||
hash := r.URL.Query().Get("hash")
|
||||
path := r.URL.Query().Get("path")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
// Decode the URL-encoded path - use PathUnescape to preserve + as literal character
|
||||
path, err := url.PathUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
// Get the file link by hash
|
||||
link, err := store.Share.GetByHash(hash)
|
||||
@@ -98,27 +103,23 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
if link.DisableFileViewer || reachedDownloadsLimit {
|
||||
getContent = false
|
||||
}
|
||||
data.shareUser, err = store.Users.Get(link.UserID)
|
||||
shareCreatedByUser, err := store.Users.Get(link.UserID)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("user for share no longer exists")
|
||||
}
|
||||
// get user scope path from share
|
||||
userScope, err := data.shareUser.GetScopeForSourceName(source.Name)
|
||||
userScope, err := shareCreatedByUser.GetScopeForSourceName(source.Name)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
// so trim user scope from link.Path
|
||||
pathWithoutUserScope := utils.JoinPathAsUnix("/", strings.TrimPrefix(link.Path, userScope), path)
|
||||
if !strings.HasSuffix(pathWithoutUserScope, "/") {
|
||||
pathWithoutUserScope = pathWithoutUserScope + "/"
|
||||
}
|
||||
data.IndexPath = pathWithoutUserScope
|
||||
if r.Method == "POST" && strings.Contains(r.URL.Path, "/resources") {
|
||||
// no need to fetch file info for uploads
|
||||
return fn(w, r, data)
|
||||
userScopedPath := utils.JoinPathAsUnix("/", strings.TrimPrefix(link.Path, userScope), path)
|
||||
if !strings.HasSuffix(userScopedPath, "/") {
|
||||
userScopedPath = userScopedPath + "/"
|
||||
}
|
||||
data.IndexPath = userScopedPath
|
||||
file, err := FileInfoFasterFunc(utils.FileOptions{
|
||||
Path: pathWithoutUserScope,
|
||||
Path: userScopedPath,
|
||||
Source: source.Name,
|
||||
Expand: true,
|
||||
Content: getContent,
|
||||
@@ -127,7 +128,7 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
ExtractEmbeddedSubtitles: settings.Config.Integrations.Media.ExtractEmbeddedSubtitles && link.ExtractEmbeddedSubtitles,
|
||||
ShowHidden: link.ShowHidden,
|
||||
FollowSymlinks: true,
|
||||
}, store.Access, data.shareUser)
|
||||
}, store.Access, shareCreatedByUser)
|
||||
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")
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -45,8 +44,6 @@ func (u *userInfoUnmarshaller) UnmarshalJSON(data []byte) error {
|
||||
if u.groupsClaim != "" {
|
||||
if v, ok := raw[u.groupsClaim]; ok {
|
||||
switch val := v.(type) {
|
||||
case map[string]interface{}:
|
||||
u.userInfo.Groups = slices.Collect(maps.Keys(val))
|
||||
case []interface{}:
|
||||
groups := make([]string, len(val))
|
||||
for i, g := range val {
|
||||
|
||||
@@ -80,14 +80,6 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, providedPath)
|
||||
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
|
||||
}
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(providedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
providedPath = cleanPath
|
||||
|
||||
themeMode := utils.Ternary(d.user.DarkMode, "dark", "light")
|
||||
var sourceInfo *settings.Source
|
||||
var ok bool
|
||||
@@ -110,8 +102,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
// Build file info based on whether this is a share or regular request
|
||||
// Regular user request
|
||||
logger.Debugf("OnlyOffice user request: request path=%s", path)
|
||||
var fileInfo *iteminfo.ExtendedFileInfo
|
||||
fileInfo, err = files.FileInfoFaster(utils.FileOptions{
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
@@ -345,15 +336,6 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
|
||||
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
|
||||
return returnOnlyOfficeError(w, r, 400, "missing required parameters: path + source/hash are required")
|
||||
}
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
logger.Errorf("OnlyOffice callback: invalid path: %v", err)
|
||||
return returnOnlyOfficeError(w, r, 400, "invalid path")
|
||||
}
|
||||
path = cleanPath
|
||||
|
||||
var sourceInfo *settings.Source
|
||||
var ok bool
|
||||
if d.fileInfo.Hash != "" {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/preview"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
@@ -86,6 +87,10 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
}
|
||||
|
||||
func rawFileHandler(w http.ResponseWriter, r *http.Request, file iteminfo.ExtendedFileInfo) (int, error) {
|
||||
idx := indexing.GetIndex(file.Source)
|
||||
if idx == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not found: %s", file.Source)
|
||||
}
|
||||
fd, err := os.Open(file.RealPath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
@@ -157,7 +162,7 @@ func getDirectoryPreview(r *http.Request, d *requestContext) (*iteminfo.Extended
|
||||
|
||||
func previewHelperFunc(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
previewSize := r.URL.Query().Get("size")
|
||||
if !(previewSize == "large" || previewSize == "original" || previewSize == "xlarge") {
|
||||
if !(previewSize == "large" || previewSize == "original") {
|
||||
previewSize = "small"
|
||||
}
|
||||
if !d.fileInfo.HasPreview {
|
||||
@@ -178,13 +183,9 @@ func previewHelperFunc(w http.ResponseWriter, r *http.Request, d *requestContext
|
||||
isImage := strings.HasPrefix(d.fileInfo.Type, "image")
|
||||
ext := strings.ToLower(filepath.Ext(d.fileInfo.Name))
|
||||
resizable := iteminfo.ResizableImageTypes[ext]
|
||||
|
||||
// If image is under 500KB, serve the original instead of generating a preview
|
||||
const maxSizeForOriginal = 500 * 1024 // 500KB
|
||||
if (!resizable || config.Server.DisableResize || d.fileInfo.Size < maxSizeForOriginal) && isImage {
|
||||
if (!resizable || config.Server.DisableResize) && isImage {
|
||||
return rawFileHandler(w, r, d.fileInfo)
|
||||
}
|
||||
|
||||
seekPercentage := 0
|
||||
percentage := r.URL.Query().Get("atPercentage")
|
||||
if percentage != "" {
|
||||
|
||||
+52
-61
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
|
||||
@@ -76,9 +77,9 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
|
||||
// Get all "file" parameter values (supports repeated params)
|
||||
files := r.URL.Query()["file"]
|
||||
if len(files) == 0 {
|
||||
files = []string{"/"}
|
||||
encodedFiles := r.URL.Query()["file"]
|
||||
if len(encodedFiles) == 0 {
|
||||
return http.StatusBadRequest, fmt.Errorf("no files specified")
|
||||
}
|
||||
|
||||
// Get the actual source name from the share's source mapping
|
||||
@@ -88,17 +89,16 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
actualSourceName := sourceInfo.Name
|
||||
|
||||
// Process each file path
|
||||
// Decode and process each file path
|
||||
fileList := []string{}
|
||||
for _, file := range files {
|
||||
// Rule 1: Validate each file path to prevent path traversal
|
||||
cleanFile, err := utils.SanitizeUserPath(file)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid file path: %v", err)
|
||||
for _, encodedFile := range encodedFiles {
|
||||
// Decode the URL-encoded path - use PathUnescape to preserve + as literal character
|
||||
file, decodeErr := url.PathUnescape(encodedFile)
|
||||
if decodeErr != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", decodeErr)
|
||||
}
|
||||
|
||||
// Join the share path with the requested path
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, cleanFile)
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, file)
|
||||
fileList = append(fileList, filePath)
|
||||
}
|
||||
|
||||
@@ -107,8 +107,8 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if err == errors.ErrDownloadNotAllowed {
|
||||
return http.StatusForbidden, errors.ErrDownloadNotAllowed
|
||||
}
|
||||
logger.Errorf("public share handler: error processing filelist: %v with error %v", files, err)
|
||||
return status, fmt.Errorf("error processing filelist: %v", files)
|
||||
logger.Errorf("public share handler: error processing filelist: %v with error %v", encodedFiles, err)
|
||||
return status, fmt.Errorf("error processing filelist: %v", encodedFiles)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
@@ -143,7 +143,7 @@ func publicGetResourceHandler(w http.ResponseWriter, r *http.Request, d *request
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param hash query string true "Share hash for authentication"
|
||||
// @Param path query string true "path within the share to upload to. Must be relative to share root."
|
||||
// @Param targetPath query string true "Target path within the share to upload to. Must be relative to share root."
|
||||
// @Param override query bool false "If true, overwrite existing files/folders. Defaults to false."
|
||||
// @Param action query string false "Upload action: 'override' to replace files, 'rename' to auto-rename"
|
||||
// @Param file formData file true "File to upload"
|
||||
@@ -159,15 +159,18 @@ func publicUploadHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
if d.share.ShareType != "upload" && !d.share.AllowCreate {
|
||||
return http.StatusForbidden, fmt.Errorf("uploading is disabled for this share")
|
||||
}
|
||||
|
||||
if !d.share.AllowReplacements && r.URL.Query().Get("action") == "override" {
|
||||
return http.StatusForbidden, fmt.Errorf("cannot overwrite files for this share")
|
||||
}
|
||||
// Go automatically decodes query params
|
||||
path := r.URL.Query().Get("targetPath")
|
||||
fullPath := filepath.Join(d.share.Path, path)
|
||||
source := config.Server.SourceMap[d.share.Source].Name
|
||||
// adjust query params to match resourcePostHandler
|
||||
q := r.URL.Query()
|
||||
q.Set("source", source)
|
||||
q.Set("path", d.IndexPath)
|
||||
q.Set("path", fullPath)
|
||||
r.URL.RawQuery = q.Encode()
|
||||
status, err := resourcePostHandler(w, r, d)
|
||||
if err != nil {
|
||||
@@ -246,8 +249,8 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if !d.share.AllowModify {
|
||||
return http.StatusForbidden, fmt.Errorf("create is not allowed for this share")
|
||||
}
|
||||
sourceName := d.share.GetSourceName()
|
||||
if sourceName == "" {
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
|
||||
@@ -256,15 +259,8 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
// Go automatically decodes query params
|
||||
path := r.URL.Query().Get("path")
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
|
||||
resolvedPath := utils.JoinPathAsUnix(d.share.Path, cleanPath)
|
||||
err = files.WriteFile(sourceName, resolvedPath, r.Body)
|
||||
resolvedPath := utils.JoinPathAsUnix(d.share.Path, path)
|
||||
err = files.WriteFile(source, resolvedPath, r.Body)
|
||||
// hide the error
|
||||
if err != nil {
|
||||
logger.Errorf("public put handler: error updating resource with error %v", err)
|
||||
@@ -273,26 +269,36 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// deprecated -- see publicBulkDeleteHandler
|
||||
func publicDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
if !d.share.AllowDelete {
|
||||
return http.StatusForbidden, fmt.Errorf("delete is not allowed for this share")
|
||||
}
|
||||
path := r.URL.Query().Get("path")
|
||||
indexPath := utils.JoinPathAsUnix(d.share.Path, path)
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
FollowSymlinks: true,
|
||||
Path: d.IndexPath,
|
||||
Source: d.share.Source,
|
||||
}, store.Access, d.shareUser)
|
||||
Path: indexPath,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
ShowHidden: d.share.ShowHidden,
|
||||
}, store.Access, d.user)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("resource not available")
|
||||
}
|
||||
err = files.DeleteFiles(d.share.Source, fileInfo.RealPath, fileInfo.Type == "directory")
|
||||
|
||||
// delete thumbnails
|
||||
preview.DelThumbs(r.Context(), *fileInfo)
|
||||
|
||||
err = files.DeleteFiles(source, fileInfo.RealPath, fileInfo.Type == "directory")
|
||||
if err != nil {
|
||||
logger.Errorf("public delete handler: error deleting resource with error %v", err)
|
||||
return http.StatusInternalServerError, fmt.Errorf("an error occured while deleting the resource")
|
||||
}
|
||||
// delete thumbnails
|
||||
preview.DelThumbs(r.Context(), *fileInfo)
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
@@ -314,10 +320,10 @@ func publicBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestC
|
||||
if !d.share.AllowDelete {
|
||||
return http.StatusForbidden, fmt.Errorf("delete is not allowed for this share")
|
||||
}
|
||||
|
||||
// hide the error
|
||||
status, err := resourceBulkDeleteHandler(w, r, d)
|
||||
if err != nil {
|
||||
logger.Errorf("public bulk delete handler: error deleting resources with error %v", err)
|
||||
return http.StatusInternalServerError, fmt.Errorf("an error occurred while processing the request")
|
||||
}
|
||||
return status, nil
|
||||
@@ -344,8 +350,8 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
}
|
||||
|
||||
// Get the source from the share
|
||||
sourceName := d.share.GetSourceName()
|
||||
if sourceName == "" {
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
|
||||
@@ -371,9 +377,9 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// Note: Share paths are absolute, so we don't strip user scope here
|
||||
// resourcePatchHandler will skip adding scope for shares
|
||||
for i := range req.Items {
|
||||
req.Items[i].FromSource = sourceName
|
||||
req.Items[i].FromSource = source
|
||||
req.Items[i].FromPath = utils.JoinPathAsUnix(d.share.Path, req.Items[i].FromPath)
|
||||
req.Items[i].ToSource = sourceName
|
||||
req.Items[i].ToSource = source
|
||||
req.Items[i].ToPath = utils.JoinPathAsUnix(d.share.Path, req.Items[i].ToPath)
|
||||
}
|
||||
d.Data = req
|
||||
@@ -392,15 +398,15 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return status, err
|
||||
}
|
||||
|
||||
// getShareImage serves banner or favicon files for shares as resizable previews
|
||||
// @Summary Get share image (banner or favicon) as preview
|
||||
// @Description Returns a resizable preview (large size) for the banner or favicon file of a share
|
||||
// getShareImage serves banner or favicon files for shares
|
||||
// @Summary Get share image (banner or favicon)
|
||||
// @Description Serves the banner or favicon file for a share
|
||||
// @Tags Public Shares
|
||||
// @Produce image/jpeg
|
||||
// @Produce octet-stream
|
||||
// @Param hash query string true "Share hash"
|
||||
// @Param banner query bool false "Request banner file"
|
||||
// @Param favicon query bool false "Request favicon file"
|
||||
// @Success 200 {file} file "Preview image content (JPEG)"
|
||||
// @Success 200 {file} binary "Image file content"
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 403 {object} map[string]string "Permission denied"
|
||||
// @Failure 404 {object} map[string]string "Asset not found"
|
||||
@@ -445,24 +451,9 @@ func getShareImage(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid file type, must be image")
|
||||
}
|
||||
|
||||
// Set file info in request context for preview generation
|
||||
d.fileInfo = *fileInfo
|
||||
q := r.URL.Query()
|
||||
if isBanner {
|
||||
q.Set("size", "xlarge")
|
||||
} else {
|
||||
q.Set("size", "small")
|
||||
}
|
||||
r.URL.RawQuery = q.Encode()
|
||||
|
||||
// Use the preview helper to generate and serve a resized preview
|
||||
status, err := previewHelperFunc(w, r, d)
|
||||
if err != nil {
|
||||
logger.Errorf("error generating preview for share asset: source=%v path=%v error=%v", sourceName, assetPath, err)
|
||||
return http.StatusNotFound, fmt.Errorf("preview not available for this asset")
|
||||
}
|
||||
|
||||
return status, err
|
||||
// Serve the file
|
||||
http.ServeFile(w, r, fileInfo.RealPath)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func getShareImagePartsHelper(share *share.Link, isBanner bool) (string, string, error) {
|
||||
|
||||
+35
-25
@@ -14,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
@@ -112,16 +113,6 @@ func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName stri
|
||||
func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
source := r.URL.Query().Get("source")
|
||||
fileList := r.URL.Query()["file"]
|
||||
|
||||
// Rule 1: Validate all user-provided file paths to prevent path traversal
|
||||
for i, filePath := range fileList {
|
||||
cleanPath, err := utils.SanitizeUserPath(filePath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid file path: %v", err)
|
||||
}
|
||||
fileList[i] = cleanPath
|
||||
}
|
||||
|
||||
return rawFilesHandler(w, r, d, source, fileList)
|
||||
}
|
||||
|
||||
@@ -142,6 +133,16 @@ func addFile(source string, path string, d *requestContext, tarWriter *tar.Write
|
||||
}
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
_, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
FollowSymlinks: true,
|
||||
}, store.Access, d.user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
realPath, _, _ := idx.GetRealPath(path)
|
||||
info, err := os.Stat(realPath)
|
||||
if err != nil {
|
||||
@@ -273,7 +274,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to download")
|
||||
}
|
||||
|
||||
if len(fileList) == 0 && d.share == nil {
|
||||
if len(fileList) == 0 {
|
||||
return http.StatusBadRequest, fmt.Errorf("no files specified")
|
||||
}
|
||||
|
||||
@@ -287,7 +288,6 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
var documentId string
|
||||
var logContext *OnlyOfficeLogContext
|
||||
|
||||
// modify all filepaths for user scope
|
||||
if d.share == nil {
|
||||
userscope, err = d.user.GetScopeForSourceName(source)
|
||||
if err != nil {
|
||||
@@ -309,11 +309,8 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
}
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
for i, filePath := range fileList {
|
||||
fileList[i] = utils.JoinPathAsUnix(userscope, filePath)
|
||||
}
|
||||
firstFilePath = utils.JoinPathAsUnix(userscope, firstFilePath)
|
||||
}
|
||||
firstFilePath = fileList[0]
|
||||
// For shares, the path is already correctly resolved by publicRawHandler
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
@@ -498,7 +495,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
_, err = io.Copy(w, reader)
|
||||
os.Remove(archiveData) // Remove the file after streaming
|
||||
if err != nil {
|
||||
logger.Debugf("client likely aborted: %v, failed to copy archive data to response: %v", r.Context().Err(), err)
|
||||
logger.Errorf("Failed to copy archive data to response: %v", err)
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -512,17 +509,30 @@ func computeArchiveSize(source string, fileList []string, d *requestContext) (in
|
||||
return 0, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
|
||||
var userScope string
|
||||
var err error
|
||||
if d.share == nil {
|
||||
userScope, err = d.user.GetScopeForSourceName(source)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
|
||||
}
|
||||
}
|
||||
|
||||
for _, path := range fileList {
|
||||
var fullPath string
|
||||
if d.share == nil {
|
||||
fullPath = utils.JoinPathAsUnix(userScope, path)
|
||||
// Check access control for each file in the archive
|
||||
// Silently skip if access is denied (as if the file doesn't exist)
|
||||
if store.Access != nil && !store.Access.Permitted(idx.Path, path, d.user.Username) {
|
||||
if store.Access != nil && !store.Access.Permitted(idx.Path, fullPath, d.user.Username) {
|
||||
continue // Skip this file and continue with the next one
|
||||
}
|
||||
} else {
|
||||
fullPath = path
|
||||
}
|
||||
|
||||
// For shares, the path is already correctly resolved by publicRawHandler
|
||||
realPath, isDir, err := idx.GetRealPath(path)
|
||||
realPath, isDir, err := idx.GetRealPath(fullPath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -548,11 +558,11 @@ func createZip(d *requestContext, source string, tmpDirPath string, filenames ..
|
||||
|
||||
zipWriter := zip.NewWriter(file)
|
||||
|
||||
for _, filepath := range filenames {
|
||||
err := addFile(source, filepath, d, nil, zipWriter, false)
|
||||
for _, fname := range filenames {
|
||||
err := addFile(source, fname, d, nil, zipWriter, false)
|
||||
if err != nil {
|
||||
// Access control failures return nil, so any error here is a real error
|
||||
logger.Errorf("Failed to add %s to ZIP: %v", filepath, err)
|
||||
logger.Errorf("Failed to add %s to ZIP: %v", fname, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -575,10 +585,10 @@ func createTarGz(d *requestContext, source string, tmpDirPath string, filenames
|
||||
gzWriter := gzip.NewWriter(file)
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
|
||||
for _, filepath := range filenames {
|
||||
err := addFile(source, filepath, d, tarWriter, nil, false)
|
||||
for _, fname := range filenames {
|
||||
err := addFile(source, fname, d, tarWriter, nil, false)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to add %s to TAR.GZ: %v", filepath, err)
|
||||
logger.Errorf("Failed to add %s to TAR.GZ: %v", fname, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+61
-78
@@ -203,10 +203,6 @@ type MoveCopyResponse struct {
|
||||
// @Failure 500 {object} map[string]string "Internal server error - all deletions failed"
|
||||
// @Router /api/resources/bulk/delete [post]
|
||||
func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
filePermUser := d.user
|
||||
if d.share != nil {
|
||||
filePermUser = d.shareUser
|
||||
}
|
||||
// Check permissions - either user delete permission or share delete permission
|
||||
if d.share == nil {
|
||||
if !d.user.Permissions.Delete {
|
||||
@@ -252,35 +248,24 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
}
|
||||
|
||||
if d.share != nil {
|
||||
sourceName := d.share.GetSourceName()
|
||||
if sourceName == "" {
|
||||
indexPath := utils.JoinPathAsUnix(d.share.Path, item.Path)
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
// get user scope path from share
|
||||
userScope, err := d.shareUser.GetScopeForSourceName(sourceName)
|
||||
if err != nil {
|
||||
response.Failed = append(response.Failed, BulkDeleteItem{
|
||||
Source: item.Source,
|
||||
Path: item.Path,
|
||||
Message: "user does not have access",
|
||||
})
|
||||
continue
|
||||
}
|
||||
withoutUserScope := strings.TrimPrefix(d.share.Path, userScope)
|
||||
indexPath := utils.JoinPathAsUnix(withoutUserScope, item.Path)
|
||||
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
FollowSymlinks: true,
|
||||
Path: indexPath,
|
||||
Source: sourceName,
|
||||
Source: source,
|
||||
ShowHidden: true,
|
||||
}, store.Access, filePermUser)
|
||||
}, store.Access, d.user)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("resource not available")
|
||||
}
|
||||
|
||||
// Delete the file/directory
|
||||
err = files.DeleteFiles(sourceName, fileInfo.RealPath, fileInfo.Type == "directory")
|
||||
err = files.DeleteFiles(source, fileInfo.RealPath, fileInfo.Type == "directory")
|
||||
if err != nil {
|
||||
logger.Errorf("resource bulk delete handler: error deleting file/directory: %v", err)
|
||||
response.Failed = append(response.Failed, BulkDeleteItem{
|
||||
@@ -304,7 +289,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
}
|
||||
|
||||
// Check user scope for this source
|
||||
_, err := filePermUser.GetScopeForSourceName(item.Source)
|
||||
_, err := d.user.GetScopeForSourceName(item.Source)
|
||||
if err != nil {
|
||||
response.Failed = append(response.Failed, BulkDeleteItem{
|
||||
Source: item.Source,
|
||||
@@ -330,7 +315,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
Path: idx.MakeIndexPath(item.Path, false),
|
||||
Source: item.Source,
|
||||
ShowHidden: true,
|
||||
}, store.Access, filePermUser)
|
||||
}, store.Access, d.user)
|
||||
if err != nil {
|
||||
response.Failed = append(response.Failed, BulkDeleteItem{
|
||||
Source: item.Source,
|
||||
@@ -369,7 +354,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
// @Tags Resources
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param path query string true "url encoded destination path where to place the files inside the destination source"
|
||||
// @Param path query string true "url encoded destination path where to place the files inside the destination source, a directory must end in / to create a directory"
|
||||
// @Param source query string true "Name for the desired filebrowser destination source name, default is used if not provided"
|
||||
// @Param override query bool false "Override existing file if true"
|
||||
// @Param isDir query bool false "Explicitly specify if the resource is a directory"
|
||||
@@ -382,22 +367,22 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
path := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
path = cleanPath
|
||||
|
||||
if d.share == nil && !d.user.Permissions.Create {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
|
||||
}
|
||||
filePermUser := d.user
|
||||
var err error
|
||||
accessStore := store.Access
|
||||
// if share is not nil, then set accessStore to nil
|
||||
if d.share != nil {
|
||||
filePermUser = d.shareUser
|
||||
accessStore = nil
|
||||
} else {
|
||||
// Go automatically decodes query params - no need for QueryUnescape
|
||||
if !d.user.Permissions.Create {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
|
||||
}
|
||||
// Path is now handled by FileInfoFaster which will apply user scope
|
||||
}
|
||||
isDir := r.URL.Query().Get("isDir") == "true"
|
||||
|
||||
// Determine if this is a directory based on isDir query param or trailing slash (for backwards compatibility)
|
||||
isDirParam := r.URL.Query().Get("isDir")
|
||||
isDir := isDirParam == "true" || strings.HasSuffix(path, "/")
|
||||
fileOpts := utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
@@ -409,21 +394,10 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
logger.Debugf("source %s not found", source)
|
||||
return http.StatusNotFound, fmt.Errorf("source %s not found", source)
|
||||
}
|
||||
|
||||
userscope, err := filePermUser.GetScopeForSourceName(source)
|
||||
if err != nil {
|
||||
logger.Debugf("error getting scope from source name: %v", err)
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
userscope = strings.TrimRight(userscope, "/")
|
||||
|
||||
fullIndexPath := utils.JoinPathAsUnix(userscope, path)
|
||||
|
||||
// get scoped path
|
||||
realPath, _, _ := idx.GetRealPath(fullIndexPath)
|
||||
realPath, _, _ := idx.GetRealPath(path)
|
||||
|
||||
// Check access control for the target path
|
||||
if !store.Access.Permitted(idx.Path, path, filePermUser.Username) {
|
||||
if accessStore != nil && !accessStore.Permitted(idx.Path, path, d.user.Username) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to path %s", path)
|
||||
}
|
||||
|
||||
@@ -442,6 +416,14 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
|
||||
// Directories creation on POST.
|
||||
if isDir {
|
||||
// Get user scope to resolve full index path for directory creation
|
||||
var userScope string
|
||||
userScope, err = d.user.GetScopeForSourceName(source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
fullIndexPath := utils.JoinPathAsUnix(userScope, path)
|
||||
|
||||
// Create a new FileOptions with the full index path
|
||||
dirOpts := fileOpts
|
||||
dirOpts.Path = fullIndexPath
|
||||
@@ -487,7 +469,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
}
|
||||
|
||||
var fileInfo *iteminfo.ExtendedFileInfo
|
||||
fileInfo, err = files.FileInfoFaster(fileOpts, store.Access, filePermUser)
|
||||
fileInfo, err = files.FileInfoFaster(fileOpts, accessStore, d.user)
|
||||
if err == nil { // File exists
|
||||
if r.URL.Query().Get("override") != "true" {
|
||||
logger.Debugf("resource already exists: %v", fileInfo.RealPath)
|
||||
@@ -537,16 +519,23 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
// close file before moving
|
||||
outFile.Close()
|
||||
// Move the completed file from the temp location to the final destination
|
||||
err = files.MoveResource(false, source, source, tempFilePath, realPath, store.Share, store.Access)
|
||||
err = fileutils.MoveFile(tempFilePath, realPath)
|
||||
if err != nil {
|
||||
logger.Debugf("could not move file from %v to %v: %v", tempFilePath, realPath, err)
|
||||
return http.StatusInternalServerError, fmt.Errorf("could not move file from chunked folder to destination: %v", err)
|
||||
}
|
||||
// Refresh index with user scope
|
||||
userScope, scopeErr := d.user.GetScopeForSourceName(source)
|
||||
if scopeErr == nil {
|
||||
fullIndexPath := utils.JoinPathAsUnix(userScope, fileOpts.Path)
|
||||
go files.RefreshIndex(source, fullIndexPath, false, false) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
fileInfo, err := files.FileInfoFaster(fileOpts, store.Access, filePermUser)
|
||||
fileInfo, err := files.FileInfoFaster(fileOpts, accessStore, d.user)
|
||||
if err == nil { // File exists
|
||||
if r.URL.Query().Get("override") != "true" {
|
||||
logger.Debugf("resource already exists: %v", fileInfo.RealPath)
|
||||
@@ -556,6 +545,13 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
preview.DelThumbs(r.Context(), *fileInfo)
|
||||
}
|
||||
|
||||
// Get user scope to resolve full index path for write operation
|
||||
userScope, err := d.user.GetScopeForSourceName(source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
fullIndexPath := utils.JoinPathAsUnix(userScope, path)
|
||||
|
||||
err = files.WriteFile(fileOpts.Source, fullIndexPath, r.Body)
|
||||
if err != nil {
|
||||
logger.Debugf("error writing file: %v", err)
|
||||
@@ -582,13 +578,6 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
source := r.URL.Query().Get("source")
|
||||
path := r.URL.Query().Get("path")
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
path = cleanPath
|
||||
|
||||
// Only allow PUT for files.
|
||||
if strings.HasSuffix(path, "/") {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
@@ -656,7 +645,6 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
|
||||
// Process each item
|
||||
for _, item := range req.Items {
|
||||
|
||||
// Validate all fields are provided
|
||||
if item.FromSource == "" || item.FromPath == "" || item.ToSource == "" || item.ToPath == "" {
|
||||
item.Message = "fromSource, fromPath, toSource, and toPath are required"
|
||||
@@ -669,26 +657,26 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
response.Failed = append(response.Failed, item)
|
||||
continue
|
||||
}
|
||||
cleanFromPath, err := utils.SanitizeUserPath(item.FromPath)
|
||||
if err != nil {
|
||||
item.Message = fmt.Sprintf("invalid fromPath: %v", err)
|
||||
|
||||
// Check for root path modifications
|
||||
if item.ToPath == "/" || item.FromPath == "/" {
|
||||
item.Message = "cannot modify root directory"
|
||||
if d.share != nil {
|
||||
response.Failed = append(response.Failed, MoveCopyItem{
|
||||
Message: item.Message,
|
||||
})
|
||||
continue
|
||||
}
|
||||
response.Failed = append(response.Failed, item)
|
||||
continue
|
||||
}
|
||||
cleanToPath, err := utils.SanitizeUserPath(item.ToPath)
|
||||
if err != nil {
|
||||
item.Message = fmt.Sprintf("invalid toPath: %v", err)
|
||||
response.Failed = append(response.Failed, item)
|
||||
continue
|
||||
}
|
||||
item.FromPath = cleanFromPath
|
||||
item.ToPath = cleanToPath
|
||||
|
||||
// Get user scopes for both sources
|
||||
// For shares, paths are already absolute, so use empty scope
|
||||
userscopeSrc := ""
|
||||
userscopeDst := ""
|
||||
if d.share == nil {
|
||||
var err error
|
||||
userscopeSrc, err = d.user.GetScopeForSourceName(item.FromSource)
|
||||
if err != nil {
|
||||
item.Message = "source not available"
|
||||
@@ -734,11 +722,6 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
// Build full index paths for access control
|
||||
fullSrcIndexPath := utils.JoinPathAsUnix(userscopeSrc, item.FromPath)
|
||||
fullDstIndexPath := utils.JoinPathAsUnix(userscopeDst, item.ToPath)
|
||||
if fullDstIndexPath == "/" || fullSrcIndexPath == "/" {
|
||||
item.Message = "source or destination is the root or unautharized directory"
|
||||
response.Failed = append(response.Failed, item)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check access control for both source and destination paths
|
||||
if !store.Access.Permitted(srcIdx.Path, fullSrcIndexPath, d.user.Username) {
|
||||
|
||||
@@ -122,11 +122,6 @@ func prepSearchOptions(r *http.Request, d *requestContext) (*searchOptions, erro
|
||||
sourceParam := r.URL.Query().Get("source") // deprecated, but still supported
|
||||
scope := r.URL.Query().Get("scope") // Go automatically decodes query params
|
||||
largest := r.URL.Query().Get("largest") == "true"
|
||||
cleanScope, err := utils.SanitizeUserPath(scope)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scope: %v", err)
|
||||
}
|
||||
scope = cleanScope
|
||||
|
||||
var sources []string
|
||||
if sourcesParam != "" {
|
||||
|
||||
+21
-50
@@ -33,10 +33,14 @@ type ShareResponse struct {
|
||||
}
|
||||
|
||||
// convertToFrontendShareResponse converts shares to response format with usernames
|
||||
func convertToFrontendShareResponse(r *http.Request, shares []*share.Link, user *users.User) ([]*ShareResponse, error) {
|
||||
func convertToFrontendShareResponse(r *http.Request, shares []*share.Link) ([]*ShareResponse, error) {
|
||||
responses := make([]*ShareResponse, 0, len(shares))
|
||||
for _, s := range shares {
|
||||
username := user.Username
|
||||
user, err := store.Users.Get(s.UserID)
|
||||
username := ""
|
||||
if err == nil {
|
||||
username = user.Username
|
||||
}
|
||||
|
||||
// Get source info to convert path to name for frontend
|
||||
sourceInfo, ok := config.Server.SourceMap[s.Source]
|
||||
@@ -61,9 +65,6 @@ func convertToFrontendShareResponse(r *http.Request, shares []*share.Link, user
|
||||
s.CommonShare.HasPassword = s.HasPassword()
|
||||
s.DownloadURL = getShareURL(r, s.Hash, true, s.Token)
|
||||
s.ShareURL = getShareURL(r, s.Hash, false, s.Token)
|
||||
if s.UserCanEdit(user) {
|
||||
s.CommonShare.SourceURL = s.SourceURL(user)
|
||||
}
|
||||
// Create response with source name (overrides the embedded Link's source field)
|
||||
responses = append(responses, &ShareResponse{
|
||||
Link: s,
|
||||
@@ -96,7 +97,7 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
shares = utils.NonNilSlice(shares)
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, shares, d.user)
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, shares)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -115,8 +116,13 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/share [get]
|
||||
func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
path := r.URL.Query().Get("path")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
sourceName := r.URL.Query().Get("source")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.PathUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
sourceInfo, ok := config.Server.NameToSource[sourceName] // backend source is path
|
||||
if !ok {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid source name: %s", sourceName)
|
||||
@@ -136,7 +142,7 @@ func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
|
||||
}
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, s, d.user)
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, s)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -161,16 +167,7 @@ func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
|
||||
// only allow users to delete their own shares
|
||||
thisShare, err := store.Share.GetByHash(hash)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("share not found")
|
||||
}
|
||||
if thisShare.UserID != d.user.ID && !d.user.Permissions.Admin {
|
||||
return http.StatusForbidden, fmt.Errorf("you are not allowed to delete this share")
|
||||
}
|
||||
|
||||
err = store.Share.Delete(hash)
|
||||
err := store.Share.Delete(hash)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -204,16 +201,8 @@ func sharePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext
|
||||
return http.StatusBadRequest, fmt.Errorf("hash and path are required")
|
||||
}
|
||||
|
||||
// only allow users to update their own shares
|
||||
thisShare, err := store.Share.GetByHash(body.Hash)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("share not found")
|
||||
}
|
||||
if thisShare.UserID != d.user.ID && !d.user.Permissions.Admin {
|
||||
return http.StatusForbidden, fmt.Errorf("you are not allowed to update this share")
|
||||
}
|
||||
// Update the share path
|
||||
err = store.Share.UpdateSharePath(body.Hash, body.Path)
|
||||
err := store.Share.UpdateSharePath(body.Hash, body.Path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -225,7 +214,7 @@ func sharePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{updatedShare}, d.user)
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{updatedShare})
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -374,14 +363,7 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
providedPath := body.Path
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(providedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
|
||||
body.Path = utils.JoinPathAsUnix(userscope, cleanPath)
|
||||
body.Path = utils.JoinPathAsUnix(userscope, providedPath)
|
||||
body.Path = utils.AddTrailingSlashIfNotExists(body.Path)
|
||||
// validate path exists as file or folder
|
||||
_, _, err = idx.GetRealPath(body.Path)
|
||||
@@ -405,7 +387,7 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if err = store.Share.Save(s); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{s}, d.user)
|
||||
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{s})
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -626,7 +608,8 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
return http.StatusNotFound, fmt.Errorf("share hash not found")
|
||||
}
|
||||
commonShare := shareLink.CommonShare
|
||||
commonShare.ShareURL = getShareURL(r, hash, false, "")
|
||||
commonShare.DownloadURL = getShareURL(r, hash, true, shareLink.Token)
|
||||
commonShare.ShareURL = getShareURL(r, hash, false, shareLink.Token)
|
||||
_, _, err = getShareImagePartsHelper(shareLink, true)
|
||||
if err == nil {
|
||||
commonShare.BannerUrl = fmt.Sprintf("%spublic/api/share/image?banner=true&hash=%s", config.Server.BaseURL, hash)
|
||||
@@ -637,7 +620,6 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
commonShare.Source = ""
|
||||
commonShare.Path = ""
|
||||
commonShare.DownloadURL = ""
|
||||
commonShare.SidebarLinks = []users.SidebarLink{}
|
||||
for _, link := range shareLink.SidebarLinks {
|
||||
if link.Category == "download" && shareLink.ShareType == "upload" {
|
||||
@@ -646,17 +628,6 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
commonShare.SidebarLinks = append(commonShare.SidebarLinks, link)
|
||||
}
|
||||
}
|
||||
commonShare.SourceURL = shareLink.SourceURL(d.user)
|
||||
commonShare.CanEdit = shareLink.UserCanEdit(d.user)
|
||||
if commonShare.SourceURL != "" {
|
||||
link := users.SidebarLink{
|
||||
Name: "sourceLocation",
|
||||
Category: "custom",
|
||||
Target: commonShare.SourceURL,
|
||||
}
|
||||
commonShare.SidebarLinks = append(commonShare.SidebarLinks, link)
|
||||
}
|
||||
|
||||
return renderJSON(w, r, commonShare)
|
||||
}
|
||||
|
||||
|
||||
+25
-148
@@ -3,12 +3,12 @@ package http
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
)
|
||||
|
||||
var templateRenderer *TemplateRenderer
|
||||
var cachedManifest PWAManifest // generated at startup
|
||||
|
||||
type TemplateRenderer struct {
|
||||
templates *template.Template
|
||||
@@ -25,18 +24,10 @@ type TemplateRenderer struct {
|
||||
|
||||
// Render renders a template document with headers and data
|
||||
func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data interface{}) error {
|
||||
templates := t.templates
|
||||
// If in dev mode, reload templates on every render by creating a fresh instance
|
||||
// If in dev mode, reload templates on every render.
|
||||
if t.devMode {
|
||||
var err error
|
||||
templates = template.New("").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface{}) (string, error) {
|
||||
var a []byte
|
||||
a, err = json.Marshal(v)
|
||||
return string(a), err
|
||||
},
|
||||
})
|
||||
templates, err = templates.ParseFS(assetFs, "public/index.html")
|
||||
t.templates, err = t.templates.ParseFS(assetFs, "public/index.html")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reloading template: %w", err)
|
||||
}
|
||||
@@ -47,7 +38,7 @@ func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data inter
|
||||
w.Header().Set("X-Accel-Expires", "0")
|
||||
w.Header().Set("Transfer-Encoding", "identity")
|
||||
// Execute the template with the provided data
|
||||
return templates.ExecuteTemplate(w, name, data)
|
||||
return t.templates.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestContext, file, contentType string) (int, error) {
|
||||
@@ -87,12 +78,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
disableSidebar := false
|
||||
|
||||
// Use custom favicon if configured and validated, otherwise fall back to default
|
||||
// Determine the correct favicon extension based on type
|
||||
faviconExt := ".svg" // Default to SVG
|
||||
if settings.Env.FaviconIsCustom && strings.ToLower(filepath.Ext(settings.Env.FaviconPath)) != ".svg" {
|
||||
faviconExt = ".png"
|
||||
}
|
||||
favicon := staticURL + "/favicon" + faviconExt
|
||||
favicon := staticURL + "/favicon"
|
||||
shareHash := ""
|
||||
data := make(map[string]interface{})
|
||||
disableNavButtons := settings.Config.Frontend.DisableNavButtons
|
||||
@@ -119,7 +105,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
if d.share.DisableSidebar {
|
||||
disableSidebar = true
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(d.share.Banner), "http") {
|
||||
if strings.HasPrefix(d.share.Banner, "http") {
|
||||
banner = d.share.Banner
|
||||
} else {
|
||||
_, _, err := getShareImagePartsHelper(d.share, true)
|
||||
@@ -127,7 +113,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
banner = fmt.Sprintf("%s%spublic/api/share/image?banner=true&hash=%s", config.Server.ExternalUrl, config.Server.BaseURL, d.share.Hash)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(d.share.Favicon), "http") {
|
||||
if strings.HasPrefix(d.share.Favicon, "http") {
|
||||
favicon = d.share.Favicon
|
||||
} else {
|
||||
_, _, err := getShareImagePartsHelper(d.share, false)
|
||||
@@ -188,8 +174,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
|
||||
data["htmlVars"] = map[string]interface{}{
|
||||
"title": title,
|
||||
"customCSS": template.CSS(config.Frontend.Styling.CustomCSSRaw),
|
||||
"userSelectedTheme": template.CSS(userSelectedTheme),
|
||||
"customCSS": config.Frontend.Styling.CustomCSSRaw,
|
||||
"userSelectedTheme": userSelectedTheme,
|
||||
"lightBackground": config.Frontend.Styling.LightBackground,
|
||||
"darkBackground": config.Frontend.Styling.DarkBackground,
|
||||
"staticURL": staticURL,
|
||||
@@ -200,14 +186,13 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
"winIcon": staticURL + "/mstile-256x256.png",
|
||||
"appIcon": staticURL + "/apple-touch-icon.png",
|
||||
"description": description,
|
||||
"loadingSpinnersCSS": template.CSS(loadingSpinnersCSS),
|
||||
"loadingSpinnersCSS": loadingSpinnersCSS,
|
||||
"banner": banner,
|
||||
"image": ogImage,
|
||||
"url": fullURL,
|
||||
"pwaIcon192": pwaIcon192,
|
||||
"pwaIcon256": pwaIcon256,
|
||||
"pwaIcon512": pwaIcon512,
|
||||
"manifestURL": staticURL + "/site.webmanifest",
|
||||
}
|
||||
// variables consumed by frontend as json
|
||||
data["globalVars"] = map[string]interface{}{
|
||||
@@ -246,8 +231,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
return http.StatusInternalServerError, fmt.Errorf("error marshaling globalVars: %w", err)
|
||||
}
|
||||
|
||||
// Mark as JS for html/template to avoid escaping
|
||||
data["globalVars"] = template.JS(globalVarsJSON)
|
||||
// Replace with JSON strings for direct template usage
|
||||
data["globalVars"] = string(globalVarsJSON)
|
||||
|
||||
// Render the template with global variables
|
||||
if err := templateRenderer.Render(w, file, data); err != nil {
|
||||
@@ -285,16 +270,6 @@ func setContentType(w http.ResponseWriter, path string) {
|
||||
}
|
||||
}
|
||||
|
||||
// manifestHandler serves the cached PWA manifest
|
||||
func manifestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/manifest+json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600") // Cache for 1 hour
|
||||
|
||||
if err := json.NewEncoder(w).Encode(cachedManifest); err != nil {
|
||||
http.Error(w, "Failed to serve manifest", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// staticAssetHandler serves static assets exactly as the frontend build produces them
|
||||
func staticAssetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const maxAge = 86400 // 1 day
|
||||
@@ -310,30 +285,25 @@ func staticAssetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle special routes that need path mapping
|
||||
var assetPath string
|
||||
switch path {
|
||||
case "site.webmanifest":
|
||||
manifestHandler(w, r)
|
||||
return
|
||||
case "favicon.svg":
|
||||
// For custom SVG favicons, serve the SVG directly
|
||||
case "favicon.svg", "favicon":
|
||||
// Handle custom favicon from filesystem
|
||||
if settings.Env.FaviconIsCustom {
|
||||
http.ServeFile(w, r, settings.Env.FaviconPath)
|
||||
return
|
||||
}
|
||||
// Use embedded default
|
||||
assetPath = settings.Env.FaviconEmbeddedPath
|
||||
case "pwa-icon.svg":
|
||||
// Serve custom SVG favicon for PWA if configured
|
||||
if settings.Env.FaviconIsCustom && strings.ToLower(filepath.Ext(settings.Env.FaviconPath)) == ".svg" {
|
||||
http.ServeFile(w, r, settings.Env.FaviconPath)
|
||||
return
|
||||
}
|
||||
// Use embedded default SVG
|
||||
// Fall back to default favicon SVG
|
||||
assetPath = settings.Env.FaviconEmbeddedPath
|
||||
case "favicon.png":
|
||||
if settings.Env.FaviconIsCustom && strings.ToLower(filepath.Ext(settings.Env.FaviconPath)) != ".svg" {
|
||||
iconPath := filepath.Join(settings.Env.PWAIconsDir, "pwa-icon-512.png")
|
||||
if _, err := os.Stat(iconPath); err == nil {
|
||||
http.ServeFile(w, r, iconPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Fall back to embedded default favicon.png
|
||||
assetPath = "img/icons/favicon.png"
|
||||
case "favicon-32x32.png",
|
||||
case "favicon-16x16.png", "favicon-32x32.png",
|
||||
"pwa-icon-192.png", "pwa-icon-256.png", "pwa-icon-512.png",
|
||||
"apple-touch-icon.png":
|
||||
"apple-touch-icon.png", "mstile-256x256.png":
|
||||
// Serve generated icons from cache directory
|
||||
iconPath := filepath.Join(settings.Env.PWAIconsDir, path)
|
||||
if _, err := os.Stat(iconPath); err == nil {
|
||||
@@ -342,15 +312,6 @@ func staticAssetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// Fall back to embedded favicon.png if generation failed
|
||||
assetPath = "img/icons/favicon.png"
|
||||
case "mstile-256x256.png":
|
||||
// Windows tile - redirect to pwa-icon-256.png (they're identical)
|
||||
iconPath := filepath.Join(settings.Env.PWAIconsDir, "pwa-icon-256.png")
|
||||
if _, err := os.Stat(iconPath); err == nil {
|
||||
http.ServeFile(w, r, iconPath)
|
||||
return
|
||||
}
|
||||
// Fall back to embedded favicon.png if generation failed
|
||||
assetPath = "img/icons/favicon.png"
|
||||
case "loginIcon":
|
||||
// Handle custom login icon from filesystem
|
||||
if settings.Env.LoginIconIsCustom {
|
||||
@@ -403,87 +364,3 @@ func indexHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (in
|
||||
}
|
||||
return handleWithStaticData(w, r, d, "index.html", "text/html")
|
||||
}
|
||||
|
||||
// PWAManifest represents the web app manifest structure
|
||||
type PWAManifest struct {
|
||||
Name string `json:"name"`
|
||||
ShortName string `json:"short_name"`
|
||||
Icons []PWAIcon `json:"icons"`
|
||||
StartURL string `json:"start_url"`
|
||||
Display string `json:"display"`
|
||||
BackgroundColor string `json:"background_color"`
|
||||
ThemeColor string `json:"theme_color"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// PWAIcon represents an icon in the web app manifest
|
||||
type PWAIcon struct {
|
||||
Src string `json:"src"`
|
||||
Sizes string `json:"sizes"`
|
||||
Type string `json:"type"`
|
||||
Purpose string `json:"purpose"`
|
||||
}
|
||||
|
||||
// generatePWAManifest creates the PWA manifest structure
|
||||
func generatePWAManifest(name, description, baseURL, themeColor, pwaIcon192, pwaIcon256, pwaIcon512 string) PWAManifest {
|
||||
shortName := name
|
||||
if len(name) > 12 {
|
||||
shortName = name[:12]
|
||||
}
|
||||
|
||||
return PWAManifest{
|
||||
Name: name,
|
||||
ShortName: shortName,
|
||||
StartURL: baseURL,
|
||||
Display: "standalone",
|
||||
BackgroundColor: "#ffffff",
|
||||
ThemeColor: themeColor,
|
||||
Description: description,
|
||||
Icons: []PWAIcon{
|
||||
{
|
||||
Src: pwaIcon192,
|
||||
Sizes: "192x192",
|
||||
Type: "image/png",
|
||||
Purpose: "any",
|
||||
},
|
||||
{
|
||||
Src: pwaIcon256,
|
||||
Sizes: "256x256",
|
||||
Type: "image/png",
|
||||
Purpose: "any maskable",
|
||||
},
|
||||
{
|
||||
Src: pwaIcon512,
|
||||
Sizes: "512x512",
|
||||
Type: "image/png",
|
||||
Purpose: "any",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// InitializePWAManifest generates and caches the PWA manifest at startup
|
||||
func InitializePWAManifest() {
|
||||
staticURL := config.Server.BaseURL + "public/static"
|
||||
title := config.Frontend.Name
|
||||
description := config.Frontend.Description
|
||||
defaultThemeColor := "#455a64"
|
||||
|
||||
// Determine PWA icon URLs based on custom favicon settings
|
||||
pwaIcon192 := staticURL + "/" + settings.Env.PWAIcon192
|
||||
pwaIcon256 := staticURL + "/" + settings.Env.PWAIcon256
|
||||
pwaIcon512 := staticURL + "/" + settings.Env.PWAIcon512
|
||||
|
||||
if settings.Env.FaviconIsCustom && strings.ToLower(filepath.Ext(settings.Env.FaviconPath)) == ".svg" {
|
||||
favicon := staticURL + "/favicon"
|
||||
pwaIcon192 = favicon
|
||||
pwaIcon256 = favicon
|
||||
pwaIcon512 = favicon
|
||||
} else if settings.Env.FaviconIsCustom {
|
||||
pwaIcon192 = staticURL + "/pwa-icon-192.png"
|
||||
pwaIcon256 = staticURL + "/pwa-icon-256.png"
|
||||
pwaIcon512 = staticURL + "/pwa-icon-512.png"
|
||||
}
|
||||
|
||||
cachedManifest = generatePWAManifest(title, description, config.Server.BaseURL, defaultThemeColor, pwaIcon192, pwaIcon256, pwaIcon512)
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
package icons
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/preview"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
"github.com/kovidgoyal/imaging"
|
||||
)
|
||||
|
||||
// sourceType represents the type of favicon source
|
||||
type sourceType int
|
||||
|
||||
const (
|
||||
sourceCustomSVG sourceType = iota
|
||||
sourceCustomRaster
|
||||
sourceDefault
|
||||
)
|
||||
|
||||
// iconSize defines an icon size to generate
|
||||
type iconSize struct {
|
||||
size int
|
||||
name string
|
||||
env *string
|
||||
}
|
||||
|
||||
// determineSourceType determines which type of favicon source we have
|
||||
func determineSourceType(faviconPath string) sourceType {
|
||||
if !settings.Env.FaviconIsCustom {
|
||||
return sourceDefault
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(faviconPath))
|
||||
switch ext {
|
||||
case ".svg":
|
||||
return sourceCustomSVG
|
||||
default:
|
||||
return sourceCustomRaster
|
||||
}
|
||||
}
|
||||
|
||||
// loadDefaultFavicon loads the embedded default favicon PNG
|
||||
func loadDefaultFavicon() ([]byte, error) {
|
||||
assetFs := fileutils.GetAssetFS()
|
||||
if assetFs == nil {
|
||||
return nil, errors.New("asset filesystem not initialized")
|
||||
}
|
||||
|
||||
pngPath := "img/icons/favicon.png"
|
||||
sourceData, err := fs.ReadFile(assetFs, pngPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read default favicon PNG: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Loaded default embedded favicon")
|
||||
return sourceData, nil
|
||||
}
|
||||
|
||||
// findRasterCompanion looks for a raster companion file for an SVG favicon
|
||||
// Returns the companion data and path, or error if not found
|
||||
func findRasterCompanion(svgPath string) ([]byte, string, error) {
|
||||
basePath := svgPath[:len(svgPath)-len(filepath.Ext(svgPath))]
|
||||
rasterExts := []string{".png", ".jpg", ".jpeg", ".webp", ".gif"}
|
||||
|
||||
for _, ext := range rasterExts {
|
||||
companionPath := basePath + ext
|
||||
if _, err := os.Stat(companionPath); err == nil {
|
||||
data, readErr := os.ReadFile(companionPath)
|
||||
if readErr != nil {
|
||||
continue
|
||||
}
|
||||
return data, companionPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", fmt.Errorf("no raster companion found at %s.{png,jpg,webp,gif}", basePath)
|
||||
}
|
||||
|
||||
// convertRasterToPNG decodes any raster format and re-encodes as PNG
|
||||
func convertRasterToPNG(rasterData []byte, sourceExt string) ([]byte, error) {
|
||||
img, err := imaging.Decode(bytes.NewReader(rasterData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := imaging.Encode(&buf, img, imaging.PNG); err != nil {
|
||||
return nil, fmt.Errorf("failed to convert to PNG: %w", err)
|
||||
}
|
||||
|
||||
logger.Debugf("Converted %s to PNG for icon generation", sourceExt)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// loadFaviconSource loads and prepares the favicon source data as PNG
|
||||
// Returns PNG data ready for resizing into icon sizes
|
||||
func loadFaviconSource() ([]byte, error) {
|
||||
sourceType := determineSourceType(settings.Env.FaviconPath)
|
||||
|
||||
switch sourceType {
|
||||
case sourceCustomSVG:
|
||||
logger.Debugf("Processing custom SVG favicon: %s", settings.Env.FaviconPath)
|
||||
return handleSVGFavicon(settings.Env.FaviconPath)
|
||||
|
||||
case sourceCustomRaster:
|
||||
logger.Debugf("Processing custom favicon: %s", settings.Env.FaviconPath)
|
||||
return handleRasterFavicon(settings.Env.FaviconPath)
|
||||
|
||||
case sourceDefault:
|
||||
logger.Debug("Using default embedded favicon")
|
||||
return loadDefaultFavicon()
|
||||
|
||||
default:
|
||||
return nil, errors.New("unknown icon source type")
|
||||
}
|
||||
}
|
||||
|
||||
// handleSVGFavicon processes an SVG favicon by finding its raster companion
|
||||
func handleSVGFavicon(svgPath string) ([]byte, error) {
|
||||
logger.Debug("Looking for companion for SVG favicon")
|
||||
|
||||
companionData, companionPath, err := findRasterCompanion(svgPath)
|
||||
if err != nil {
|
||||
logger.Warningf("SVG favicon has no companion: %v", err)
|
||||
logger.Warning("Falling back to default embedded favicon")
|
||||
logger.Warning("For best Apple/iOS compatibility, provide a PNG/JPG companion alongside your SVG")
|
||||
return loadDefaultFavicon()
|
||||
}
|
||||
|
||||
logger.Debugf("Found companion: %s", companionPath)
|
||||
return convertRasterToPNG(companionData, filepath.Ext(companionPath))
|
||||
}
|
||||
|
||||
// handleRasterFavicon processes a raster favicon (PNG, JPEG, GIF, WebP, etc.)
|
||||
func handleRasterFavicon(faviconPath string) ([]byte, error) {
|
||||
sourceData, err := os.ReadFile(faviconPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read favicon: %w", err)
|
||||
}
|
||||
|
||||
return convertRasterToPNG(sourceData, filepath.Ext(faviconPath))
|
||||
}
|
||||
|
||||
// getIconSizesToGenerate returns all icon sizes that need to be generated
|
||||
func getIconSizesToGenerate() []iconSize {
|
||||
return []iconSize{
|
||||
// Browser favicon (32x32 PNG for Safari/older browsers)
|
||||
{32, "favicon-32x32.png", nil},
|
||||
// PWA icons (PNG required for Apple PWA compatibility)
|
||||
{192, "pwa-icon-192.png", &settings.Env.PWAIcon192},
|
||||
{256, "pwa-icon-256.png", &settings.Env.PWAIcon256}, // Also serves as Windows tile
|
||||
{512, "pwa-icon-512.png", &settings.Env.PWAIcon512},
|
||||
// Platform-specific icons
|
||||
{180, "apple-touch-icon.png", nil}, // iOS home screen (required for Apple)
|
||||
}
|
||||
}
|
||||
|
||||
// generateIconSizes generates all required icon sizes from source PNG data
|
||||
func generateIconSizes(sourceData []byte) error {
|
||||
previewService := preview.GetService()
|
||||
if previewService == nil {
|
||||
return errors.New("preview service not initialized")
|
||||
}
|
||||
|
||||
iconSizes := getIconSizesToGenerate()
|
||||
allSuccess := true
|
||||
generatedCount := 0
|
||||
|
||||
for _, icon := range iconSizes {
|
||||
if err := generateSingleIcon(previewService, sourceData, icon); err != nil {
|
||||
logger.Warningf("Failed to generate %s: %v", icon.name, err)
|
||||
allSuccess = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Update environment variable if specified
|
||||
if icon.env != nil {
|
||||
*icon.env = filepath.Join("icons", icon.name)
|
||||
}
|
||||
generatedCount++
|
||||
}
|
||||
|
||||
if allSuccess {
|
||||
logger.Debugf("Successfully generated %d icon sizes", generatedCount)
|
||||
} else {
|
||||
logger.Warningf("Generated %d/%d icon sizes (some failed)", generatedCount, len(iconSizes))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateSingleIcon generates a single icon file at the specified size
|
||||
func generateSingleIcon(previewService *preview.Service, sourceData []byte, icon iconSize) error {
|
||||
outputPath := filepath.Join(settings.Env.PWAIconsDir, icon.name)
|
||||
|
||||
// Create output file
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// Resize image using the preview service's Resize method
|
||||
err = previewService.Resize(
|
||||
bytes.NewReader(sourceData),
|
||||
outFile,
|
||||
preview.ResizeOptions{
|
||||
Width: icon.size,
|
||||
Height: icon.size,
|
||||
ResizeMode: preview.ResizeModeFill,
|
||||
Quality: preview.QualityHigh,
|
||||
Format: preview.FormatPng,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("resize failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GeneratePWAIcons generates all PWA and platform icon sizes from favicon source
|
||||
// All icons are generated as PNG for maximum compatibility (Apple devices, PWA, older browsers)
|
||||
func GeneratePWAIcons() error {
|
||||
logger.Info("Generating PWA icons...")
|
||||
// Set PWA icons directory in cache
|
||||
settings.Env.PWAIconsDir = filepath.Join(settings.Config.Server.CacheDir, "icons")
|
||||
|
||||
// Initialize with default paths (all icons will be generated)
|
||||
settings.Env.PWAIcon192 = "icons/pwa-icon-192.png"
|
||||
settings.Env.PWAIcon256 = "icons/pwa-icon-256.png"
|
||||
settings.Env.PWAIcon512 = "icons/pwa-icon-512.png"
|
||||
|
||||
// Create icons directory with configured permissions
|
||||
// Note: Parent cache directory should already exist from testCacheDirSpeed()
|
||||
if err := os.MkdirAll(settings.Env.PWAIconsDir, fileutils.PermDir); err != nil {
|
||||
logger.Warningf("Failed to create PWA icons directory %s: %v", settings.Env.PWAIconsDir, err)
|
||||
}
|
||||
// Load and prepare source data as PNG
|
||||
sourceData, err := loadFaviconSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load favicon source: %w", err)
|
||||
}
|
||||
|
||||
// Generate all icon sizes
|
||||
if err := generateIconSizes(sourceData); err != nil {
|
||||
return fmt.Errorf("failed to generate icon sizes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -196,7 +196,7 @@ func TestProcessDirectoryItem(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
itemInfo, size, shouldCount := idx.processDirectoryItem(
|
||||
tt.file, tt.combinedPath,
|
||||
@@ -212,7 +212,6 @@ func TestProcessDirectoryItem(t *testing.T) {
|
||||
|
||||
if itemInfo == nil {
|
||||
t.Fatalf("expected non-nil itemInfo")
|
||||
return
|
||||
}
|
||||
|
||||
if size != tt.expectSize {
|
||||
|
||||
@@ -3,7 +3,7 @@ package iteminfo
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
|
||||
)
|
||||
|
||||
type ItemInfo struct {
|
||||
@@ -40,21 +40,21 @@ type MediaMetadata struct {
|
||||
Genre string `json:"genre,omitempty"` // music/video genre
|
||||
Track int `json:"track,omitempty"` // track number
|
||||
Duration int `json:"duration,omitempty"` // duration in seconds
|
||||
AlbumArt []byte `json:"albumArt,omitempty"` // album art image data (automatically base64-encoded in JSON)
|
||||
AlbumArt string `json:"albumArt,omitempty"` // base64 encoded album art / video thumbnail
|
||||
}
|
||||
|
||||
// for efficiency, a response will be a pointer to the data
|
||||
// extra calculated fields can be added here
|
||||
type ExtendedFileInfo struct {
|
||||
FileInfo
|
||||
Content string `json:"content,omitempty"` // text content of a file, if requested
|
||||
Subtitles []utils.SubtitleTrack `json:"subtitles,omitempty"` // subtitles for video files
|
||||
Metadata *MediaMetadata `json:"metadata,omitempty"` // media metadata for audio/video files (includes duration)
|
||||
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
|
||||
HasMetadata bool `json:"hasMetadata"` // whether the file or folder has metadata
|
||||
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
|
||||
Metadata *MediaMetadata `json:"metadata,omitempty"` // media metadata for audio/video files (includes duration)
|
||||
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
|
||||
HasMetadata bool `json:"hasMetadata"` // whether the file or folder has metadata
|
||||
RealPath string `json:"-"`
|
||||
}
|
||||
@@ -8,9 +8,19 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"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"`
|
||||
@@ -21,55 +31,20 @@ type FFProbeOutput struct {
|
||||
} `json:"streams"`
|
||||
}
|
||||
|
||||
// GetSubtitles detects external subtitle files for video files.
|
||||
// Embedded subtitles should be detected by ffmpeg and passed as embeddedSubs parameter.
|
||||
func (i *ExtendedFileInfo) GetSubtitles(parentInfo *FileInfo) {
|
||||
// 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)
|
||||
// Use unified subtitle detection that finds both embedded and external
|
||||
parentDir := filepath.Dir(i.RealPath)
|
||||
i.Subtitles = ffmpeg.DetectAllSubtitles(i.RealPath, parentDir, i.ModTime)
|
||||
}
|
||||
|
||||
// Collect external subtitle files
|
||||
var externalSubs []utils.SubtitleTrack
|
||||
if parentInfo != nil && parentInfo.Files != nil {
|
||||
for _, f := range parentInfo.Files {
|
||||
fileExt := filepath.Ext(f.Name)
|
||||
fileBase := strings.TrimSuffix(f.Name, fileExt)
|
||||
|
||||
// Check if this file has the same base name and a subtitle extension
|
||||
if fileBase == baseWithoutExt && SubtitleExts[strings.ToLower(fileExt)] {
|
||||
track := utils.SubtitleTrack{
|
||||
Name: f.Name,
|
||||
Embedded: false,
|
||||
}
|
||||
|
||||
// Try to infer language from filename patterns like "video.en.srt"
|
||||
parts := strings.Split(fileBase, ".")
|
||||
if len(parts) > 1 {
|
||||
lastPart := parts[len(parts)-1]
|
||||
if len(lastPart) == 2 || len(lastPart) == 3 {
|
||||
track.Language = lastPart
|
||||
}
|
||||
}
|
||||
|
||||
externalSubs = append(externalSubs, track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort external subtitles alphabetically for consistent ordering
|
||||
sort.Slice(externalSubs, func(i, j int) bool {
|
||||
return externalSubs[i].Name < externalSubs[j].Name
|
||||
})
|
||||
|
||||
i.Subtitles = append(i.Subtitles, externalSubs...)
|
||||
|
||||
// Sort all subtitles for consistent ordering
|
||||
sort.Slice(i.Subtitles, func(a, b int) bool {
|
||||
return strings.ToLower(i.Subtitles[a].Name) < strings.ToLower(i.Subtitles[b].Name)
|
||||
})
|
||||
// Note: Content is NOT loaded here. Frontend will call /api/media/subtitles for each track
|
||||
// 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() {
|
||||
|
||||
@@ -174,10 +174,6 @@ func (s *Service) ResizeWithSize(in io.Reader, out io.Writer, fileSize int64, op
|
||||
if opts.Format == FormatHeic {
|
||||
opts.Format = FormatJpeg
|
||||
}
|
||||
// Ensure JPEG quality is set based on quality setting
|
||||
if opts.JpegQuality == 0 {
|
||||
opts.JpegQuality = opts.Quality.jpegQuality()
|
||||
}
|
||||
|
||||
// Try to use embedded EXIF thumbnail for JPEGs (only for low quality to keep it simple)
|
||||
if opts.Format == FormatJpeg && opts.Quality == QualityLow {
|
||||
|
||||
@@ -489,17 +489,12 @@ func TestCreatePreviewFromReader_UsesConcurrencyLimit(t *testing.T) {
|
||||
data, err := io.ReadAll(source)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get options for small preview
|
||||
options, err := getPreviewOptions("small")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create preview from reader
|
||||
out, err := svc.CreatePreview(bytes.NewReader(data), int64(len(data)), options)
|
||||
out, err := svc.CreatePreviewFromReader(bytes.NewReader(data), "small")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, out)
|
||||
|
||||
// Same via another call for parity
|
||||
out2, err := svc.CreatePreview(bytes.NewReader(data), int64(len(data)), options)
|
||||
// Same via CreatePreview (bytes) for parity
|
||||
out2, err := svc.CreatePreview(data, "small")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, out, out2)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package preview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// MemoryTracker wraps the existing FFmpegService to add memory tracking
|
||||
type MemoryTracker struct {
|
||||
maxMemoryBytes int64
|
||||
usedMemoryBytes int64
|
||||
semaphore chan struct{}
|
||||
}
|
||||
|
||||
// NewMemoryTracker creates a memory-aware concurrency limiter
|
||||
func NewMemoryTracker(maxConcurrent int, maxMemoryMB int) *MemoryTracker {
|
||||
if maxConcurrent < 1 {
|
||||
maxConcurrent = 1
|
||||
}
|
||||
if maxMemoryMB < 50 {
|
||||
maxMemoryMB = 500 // Default 500MB
|
||||
}
|
||||
return &MemoryTracker{
|
||||
maxMemoryBytes: int64(maxMemoryMB) * 1024 * 1024,
|
||||
semaphore: make(chan struct{}, maxConcurrent),
|
||||
}
|
||||
}
|
||||
|
||||
// TryAcquire attempts to acquire resources for processing an image
|
||||
// Returns false if memory limit would be exceeded
|
||||
func (mt *MemoryTracker) TryAcquire(ctx context.Context, estimatedBytes int64) bool {
|
||||
// Check if we have memory headroom
|
||||
currentUsed := atomic.LoadInt64(&mt.usedMemoryBytes)
|
||||
if currentUsed+estimatedBytes > mt.maxMemoryBytes {
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to acquire semaphore slot (non-blocking check first)
|
||||
select {
|
||||
case mt.semaphore <- struct{}{}:
|
||||
// Successfully acquired slot, now add memory
|
||||
atomic.AddInt64(&mt.usedMemoryBytes, estimatedBytes)
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire waits to acquire resources
|
||||
func (mt *MemoryTracker) Acquire(ctx context.Context, estimatedBytes int64) error {
|
||||
// Wait for semaphore slot
|
||||
select {
|
||||
case mt.semaphore <- struct{}{}:
|
||||
atomic.AddInt64(&mt.usedMemoryBytes, estimatedBytes)
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Release frees up resources
|
||||
func (mt *MemoryTracker) Release(estimatedBytes int64) {
|
||||
<-mt.semaphore
|
||||
atomic.AddInt64(&mt.usedMemoryBytes, -estimatedBytes)
|
||||
}
|
||||
|
||||
// GetMemoryUsageMB returns current memory usage in MB
|
||||
func (mt *MemoryTracker) GetMemoryUsageMB() int64 {
|
||||
return atomic.LoadInt64(&mt.usedMemoryBytes) / (1024 * 1024)
|
||||
}
|
||||
+316
-241
@@ -4,10 +4,12 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -20,6 +22,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
"github.com/kovidgoyal/imaging"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -161,6 +164,10 @@ func StartPreviewGenerator(concurrencyLimit int, cacheDir string) error {
|
||||
logger.Errorf("WARNING: StartPreviewGenerator called multiple times! This will create multiple semaphores!")
|
||||
}
|
||||
service = NewPreviewGenerator(concurrencyLimit, cacheDir)
|
||||
|
||||
// Generate PWA icons after service is initialized
|
||||
GeneratePWAIcons()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -169,6 +176,153 @@ func GetService() *Service {
|
||||
return service
|
||||
}
|
||||
|
||||
// GeneratePWAIcons generates all icon sizes from favicon.svg or favicon.png
|
||||
func GeneratePWAIcons() {
|
||||
if service == nil {
|
||||
logger.Warning("Preview service not initialized, skipping icon generation")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine source icon - prefer custom, fallback to default
|
||||
var sourceIcon string
|
||||
var sourceData []byte
|
||||
var err error
|
||||
var isSVG bool
|
||||
|
||||
if settings.Env.FaviconIsCustom {
|
||||
sourceIcon = settings.Env.FaviconPath
|
||||
logger.Debugf("Generating icons from custom favicon: %s", sourceIcon)
|
||||
isSVG = strings.ToLower(filepath.Ext(sourceIcon)) == ".svg"
|
||||
|
||||
// Read the source file
|
||||
sourceData, err = os.ReadFile(sourceIcon)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to read custom favicon: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If it's not a raster format that imaging library can decode, handle separately
|
||||
if isSVG {
|
||||
// SVG: Set PWA icons to use SVG directly, look for PNG for raster generation
|
||||
logger.Debug("Source is SVG - will use directly where supported")
|
||||
settings.Env.PWAIcon192 = "pwa-icon.svg"
|
||||
settings.Env.PWAIcon256 = "pwa-icon.svg"
|
||||
settings.Env.PWAIcon512 = "pwa-icon.svg"
|
||||
|
||||
// Look for PNG in same directory with same base name
|
||||
basePath := sourceIcon[:len(sourceIcon)-len(filepath.Ext(sourceIcon))]
|
||||
pngPath := basePath + ".png"
|
||||
sourceData, err = os.ReadFile(pngPath)
|
||||
if err != nil {
|
||||
logger.Warningf("SVG favicon provided without PNG version at %s. For best compatibility across all browsers and platforms, provide a PNG version alongside your SVG.", pngPath)
|
||||
return
|
||||
}
|
||||
logger.Debugf("Using PNG version (%s) for generating raster icons", pngPath)
|
||||
} else {
|
||||
// For any raster format (JPEG, PNG, GIF, WebP, etc.), decode and convert to PNG
|
||||
// This creates a standardized PNG source for all icon generation
|
||||
img, decodeErr := imaging.Decode(bytes.NewReader(sourceData))
|
||||
if decodeErr != nil {
|
||||
logger.Warningf("Failed to decode favicon image: %v", decodeErr)
|
||||
return
|
||||
}
|
||||
// Re-encode as PNG in memory for consistent processing
|
||||
var buf bytes.Buffer
|
||||
if encodeErr := imaging.Encode(&buf, img, imaging.PNG); encodeErr != nil {
|
||||
logger.Warningf("Failed to convert favicon to PNG: %v", encodeErr)
|
||||
return
|
||||
}
|
||||
sourceData = buf.Bytes()
|
||||
logger.Debugf("Converted %s favicon to PNG for icon generation", filepath.Ext(sourceIcon))
|
||||
}
|
||||
} else {
|
||||
// Use default embedded favicon - we have both SVG and PNG versions
|
||||
logger.Debug("Generating icons from default favicon")
|
||||
// For default, use the PNG version for raster generation
|
||||
assetFs := fileutils.GetAssetFS()
|
||||
if assetFs == nil {
|
||||
logger.Warning("Asset filesystem not initialized, skipping default icon generation")
|
||||
return
|
||||
}
|
||||
pngPath := "img/icons/favicon.png"
|
||||
sourceData, err = fs.ReadFile(assetFs, pngPath)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to read default favicon PNG: %v", err)
|
||||
return
|
||||
}
|
||||
// Set PWA icons to use SVG for modern browsers
|
||||
settings.Env.PWAIcon192 = "pwa-icon.svg"
|
||||
settings.Env.PWAIcon256 = "pwa-icon.svg"
|
||||
settings.Env.PWAIcon512 = "pwa-icon.svg"
|
||||
}
|
||||
|
||||
// Define all icon sizes we need to generate
|
||||
// Format: {size, filename, envVar pointer (optional)}
|
||||
iconSizes := []struct {
|
||||
size int
|
||||
name string
|
||||
env *string
|
||||
}{
|
||||
// Browser favicon
|
||||
{32, "favicon-32x32.png", nil},
|
||||
// PWA icons
|
||||
{192, "pwa-icon-192.png", &settings.Env.PWAIcon192},
|
||||
{256, "pwa-icon-256.png", &settings.Env.PWAIcon256},
|
||||
{512, "pwa-icon-512.png", &settings.Env.PWAIcon512},
|
||||
// Platform-specific icons
|
||||
{180, "apple-touch-icon.png", nil}, // iOS home screen
|
||||
{256, "mstile-256x256.png", nil}, // Windows tile
|
||||
}
|
||||
|
||||
allSuccess := true
|
||||
generatedCount := 0
|
||||
|
||||
for _, iconSize := range iconSizes {
|
||||
outputPath := filepath.Join(settings.Env.PWAIconsDir, iconSize.name)
|
||||
|
||||
// Create output file
|
||||
outFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to create icon %s: %v", iconSize.name, err)
|
||||
allSuccess = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Resize image using the service's Resize method
|
||||
err = service.Resize(
|
||||
bytes.NewReader(sourceData),
|
||||
outFile,
|
||||
ResizeOptions{
|
||||
Width: iconSize.size,
|
||||
Height: iconSize.size,
|
||||
ResizeMode: ResizeModeFill,
|
||||
Quality: QualityHigh,
|
||||
Format: FormatPng,
|
||||
},
|
||||
)
|
||||
outFile.Close()
|
||||
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to generate icon %s: %v", iconSize.name, err)
|
||||
os.Remove(outputPath)
|
||||
allSuccess = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Update environment variable if specified
|
||||
if iconSize.env != nil {
|
||||
*iconSize.env = filepath.Join("icons", iconSize.name)
|
||||
}
|
||||
generatedCount++
|
||||
}
|
||||
|
||||
if allSuccess {
|
||||
logger.Debugf("Successfully generated %d icon sizes", generatedCount)
|
||||
} else {
|
||||
logger.Warningf("Generated %d/%d icon sizes (some failed)", generatedCount, len(iconSizes))
|
||||
}
|
||||
}
|
||||
|
||||
func GetPreviewForFile(ctx context.Context, file iteminfo.ExtendedFileInfo, previewSize, url string, seekPercentage int) ([]byte, error) {
|
||||
if !file.HasPreview {
|
||||
return nil, ErrUnsupportedMedia
|
||||
@@ -181,10 +335,10 @@ func GetPreviewForFile(ctx context.Context, file iteminfo.ExtendedFileInfo, prev
|
||||
|
||||
// Generate fast cache key based on file metadata
|
||||
var cacheHash string
|
||||
if file.Metadata != nil && len(file.Metadata.AlbumArt) > 0 {
|
||||
if file.Metadata != nil && file.Metadata.AlbumArt != "" {
|
||||
// For audio with album art, hash the album art content
|
||||
hasher := md5.New()
|
||||
_, _ = hasher.Write(file.Metadata.AlbumArt)
|
||||
_, _ = hasher.Write([]byte(file.Metadata.AlbumArt))
|
||||
cacheHash = hex.EncodeToString(hasher.Sum(nil))
|
||||
} else {
|
||||
// For all other files, use fast metadata-based hash
|
||||
@@ -203,191 +357,6 @@ func GetPreviewForFile(ctx context.Context, file iteminfo.ExtendedFileInfo, prev
|
||||
return GeneratePreviewWithMD5(ctx, file, previewSize, url, seekPercentage, cacheHash)
|
||||
}
|
||||
|
||||
// filePreviewType represents the type of preview generation needed
|
||||
type filePreviewType int
|
||||
|
||||
const (
|
||||
previewTypeDocument filePreviewType = iota
|
||||
previewTypeOffice
|
||||
previewTypeHEIC
|
||||
previewTypeImage
|
||||
previewTypeVideo
|
||||
previewTypeAudio
|
||||
previewTypeUnsupported
|
||||
)
|
||||
|
||||
// determinePreviewType determines what type of preview generation is needed
|
||||
func determinePreviewType(file iteminfo.ExtendedFileInfo) filePreviewType {
|
||||
// Check document conversion first
|
||||
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
|
||||
return previewTypeDocument
|
||||
}
|
||||
|
||||
// Check office files
|
||||
if file.OnlyOfficeId != "" {
|
||||
return previewTypeOffice
|
||||
}
|
||||
|
||||
// Check HEIC with conversion enabled
|
||||
if strings.HasPrefix(file.Type, "image/heic") &&
|
||||
*settings.Config.Integrations.Media.Convert.ImagePreview[settings.HEICImagePreview] {
|
||||
return previewTypeHEIC
|
||||
}
|
||||
|
||||
// Check by MIME type prefix
|
||||
switch {
|
||||
case strings.HasPrefix(file.Type, "image"):
|
||||
return previewTypeImage
|
||||
case strings.HasPrefix(file.Type, "video"):
|
||||
return previewTypeVideo
|
||||
case strings.HasPrefix(file.Type, "audio") && file.Metadata != nil && len(file.Metadata.AlbumArt) > 0:
|
||||
return previewTypeAudio
|
||||
default:
|
||||
return previewTypeUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
// generateRawPreview generates the initial preview image bytes based on file type
|
||||
func (s *Service) generateRawPreview(ctx context.Context, file iteminfo.ExtendedFileInfo, previewSize, officeUrl string, seekPercentage int, hash string) ([]byte, error) {
|
||||
previewType := determinePreviewType(file)
|
||||
|
||||
switch previewType {
|
||||
case previewTypeDocument:
|
||||
return s.generateDocumentPreview(ctx, file, hash)
|
||||
|
||||
case previewTypeOffice:
|
||||
return s.generateOfficeFilePreview(ctx, file, officeUrl)
|
||||
|
||||
case previewTypeHEIC:
|
||||
return s.generateHEICPreview(ctx, file, previewSize)
|
||||
|
||||
case previewTypeImage:
|
||||
return s.generateImagePreview(ctx, file, previewSize)
|
||||
|
||||
case previewTypeVideo:
|
||||
return s.generateVideoPreviewBytes(ctx, file, seekPercentage)
|
||||
|
||||
case previewTypeAudio:
|
||||
if file.Metadata != nil && len(file.Metadata.AlbumArt) > 0 {
|
||||
return file.Metadata.AlbumArt, nil
|
||||
}
|
||||
return nil, nil
|
||||
case previewTypeUnsupported:
|
||||
ext := strings.ToLower(filepath.Ext(file.Name))
|
||||
return nil, fmt.Errorf("unsupported media type: %s", ext)
|
||||
|
||||
default:
|
||||
return nil, errors.New("unknown preview type")
|
||||
}
|
||||
}
|
||||
|
||||
// generateDocumentPreview generates preview for PDF and document files
|
||||
func (s *Service) generateDocumentPreview(ctx context.Context, file iteminfo.ExtendedFileInfo, hash string) ([]byte, error) {
|
||||
tempFilePath := filepath.Join(s.cacheDir, "thumbnails", "docs", hash) + ".txt"
|
||||
imageBytes, err := s.GenerateImageFromDoc(ctx, file, tempFilePath, 0) // 0 for the first page
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image for PDF file: %w", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
// generateOfficeFilePreview generates preview for Office files via OnlyOffice
|
||||
func (s *Service) generateOfficeFilePreview(ctx context.Context, file iteminfo.ExtendedFileInfo, officeUrl string) ([]byte, error) {
|
||||
imageBytes, err := s.GenerateOfficePreview(ctx, filepath.Ext(file.Name), file.OnlyOfficeId, file.Name, officeUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image for office file: %w", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
// generateHEICPreview generates preview for HEIC files using FFmpeg
|
||||
func (s *Service) generateHEICPreview(ctx context.Context, file iteminfo.ExtendedFileInfo, previewSize string) ([]byte, error) {
|
||||
imageBytes, err := s.convertHEICToJPEGWithFFmpeg(ctx, file.RealPath, previewSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process HEIC image file: %w", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
// generateVideoPreviewBytes generates preview frame from video file
|
||||
func (s *Service) generateVideoPreviewBytes(ctx context.Context, file iteminfo.ExtendedFileInfo, seekPercentage int) ([]byte, error) {
|
||||
// Check if this video format is enabled for preview generation
|
||||
ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(file.Name)), ".")
|
||||
if !settings.CanConvertVideo(ext) {
|
||||
return nil, fmt.Errorf("video preview generation is disabled for .%s files in settings", ext)
|
||||
}
|
||||
|
||||
videoSeekPercentage := seekPercentage
|
||||
if videoSeekPercentage == 0 {
|
||||
videoSeekPercentage = 10
|
||||
}
|
||||
|
||||
imageBytes, err := s.GenerateVideoPreview(ctx, file.RealPath, videoSeekPercentage)
|
||||
if err != nil {
|
||||
// Don't log client cancellations as errors
|
||||
if ctx.Err() != context.Canceled {
|
||||
logger.Errorf("Video preview generation failed for '%s' (path: %s, seek: %d%%): %v",
|
||||
file.Name, file.RealPath, videoSeekPercentage, err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to create image for video file: %w", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
// generateImagePreview generates preview for regular image files
|
||||
func (s *Service) generateImagePreview(ctx context.Context, file iteminfo.ExtendedFileInfo, previewSize string) ([]byte, error) {
|
||||
// Stream from file instead of os.ReadFile so we don't load every image fully into memory
|
||||
f, err := os.Open(file.RealPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to open image file '%s' (path: %s): %v", file.Name, file.RealPath, err)
|
||||
return nil, fmt.Errorf("failed to open image file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
options, err := getPreviewOptions(previewSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageBytes, err := s.CreatePreview(f, file.Size, options)
|
||||
if err != nil {
|
||||
// For JPEG files, try FFmpeg fallback if initial decode failed
|
||||
if strings.HasPrefix(file.Type, "image/jpeg") {
|
||||
return handleJPEGFallback(ctx, s, file, previewSize, err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to create image preview: %w", err)
|
||||
}
|
||||
|
||||
if len(imageBytes) < 100 {
|
||||
logger.Errorf("Generated image too small for '%s' (type: %s): %d bytes", file.Name, file.Type, len(imageBytes))
|
||||
return nil, fmt.Errorf("generated image is too small, likely an error occurred: %d bytes", len(imageBytes))
|
||||
}
|
||||
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
// handleJPEGFallback attempts to use FFmpeg for problematic JPEG files
|
||||
// This is preview-specific logic for user files, not used for icon generation
|
||||
func handleJPEGFallback(ctx context.Context, s *Service, file iteminfo.ExtendedFileInfo, previewSize string, originalErr error) ([]byte, error) {
|
||||
enableJPEGFallback := *settings.Config.Integrations.Media.Convert.ImagePreview[settings.JPEGImagePreview]
|
||||
|
||||
if !enableJPEGFallback {
|
||||
return nil, fmt.Errorf("failed to resize preview image (unsupported JPEG format, FFmpeg conversion disabled in settings): %w", originalErr)
|
||||
}
|
||||
|
||||
if s.ffmpegService == nil {
|
||||
return nil, fmt.Errorf("failed to resize preview image (unsupported JPEG format, FFmpeg not available): %w", originalErr)
|
||||
}
|
||||
|
||||
logger.Debugf("JPEG decode failed for '%s', falling back to FFmpeg: %v", file.Name, originalErr)
|
||||
imageBytes, err := s.convertImageWithFFmpeg(ctx, file.RealPath, previewSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resize preview image with FFmpeg fallback: %w", err)
|
||||
}
|
||||
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
func GeneratePreviewWithMD5(ctx context.Context, file iteminfo.ExtendedFileInfo, previewSize, officeUrl string, seekPercentage int, fileMD5 string) ([]byte, error) {
|
||||
// Note: fileMD5 is actually a cache hash (metadata-based), not a true file content MD5
|
||||
// Validate that cache hash is not empty to prevent cache corruption
|
||||
@@ -405,11 +374,13 @@ func GeneratePreviewWithMD5(ctx context.Context, file iteminfo.ExtendedFileInfo,
|
||||
// Acquire global image processor semaphore for ALL operations
|
||||
const largeFileSizeThreshold = 8 * 1024 * 1024 // 8MB
|
||||
if file.Size >= largeFileSizeThreshold && service.imageLargeSem != nil {
|
||||
// Large file path - use imageLargeSem
|
||||
if err := service.acquireImageLargeSem(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer service.releaseImageLargeSem()
|
||||
} else {
|
||||
// Small file path or fallback - use imageSem
|
||||
if err := service.acquireImageSem(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -429,34 +400,122 @@ func GeneratePreviewWithMD5(ctx context.Context, file iteminfo.ExtendedFileInfo,
|
||||
return nil, errors.New(message)
|
||||
}
|
||||
|
||||
// Generate hash for temp file paths
|
||||
ext := strings.ToLower(filepath.Ext(file.Name))
|
||||
var (
|
||||
err error
|
||||
imageBytes []byte
|
||||
)
|
||||
|
||||
// Generate thumbnail image from video
|
||||
hasher := md5.New()
|
||||
_, _ = hasher.Write([]byte(CacheKey(fileMD5, previewSize, seekPercentage)))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
// Generate raw preview based on file type
|
||||
imageBytes, err := service.generateRawPreview(ctx, file, previewSize, officeUrl, seekPercentage, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check if HEIC conversion is enabled (guaranteed non-nil after defaults)
|
||||
convertHEIC := strings.HasPrefix(file.Type, "image/heic") && *settings.Config.Integrations.Media.Convert.ImagePreview[settings.HEICImagePreview]
|
||||
|
||||
// For HEIC files, we've already done the resize/conversion, cache and return directly
|
||||
previewType := determinePreviewType(file)
|
||||
if previewType == previewTypeHEIC {
|
||||
// Generate an image from office document
|
||||
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
|
||||
tempFilePath := filepath.Join(service.cacheDir, "thumbnails", "docs", hash) + ".txt"
|
||||
imageBytes, err = service.GenerateImageFromDoc(ctx, file, tempFilePath, 0) // 0 for the first page
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image for PDF file: %w", err)
|
||||
}
|
||||
} else if file.OnlyOfficeId != "" {
|
||||
imageBytes, err = service.GenerateOfficePreview(ctx, filepath.Ext(file.Name), file.OnlyOfficeId, file.Name, officeUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image for office file: %w", err)
|
||||
}
|
||||
} else if convertHEIC {
|
||||
// HEIC files need FFmpeg conversion to JPEG with proper size/quality handling
|
||||
imageBytes, err = service.convertHEICToJPEGWithFFmpeg(ctx, file.RealPath, previewSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process HEIC image file: %w", err)
|
||||
}
|
||||
// For HEIC files, we've already done the resize/conversion, so cache and return directly
|
||||
cacheKey := CacheKey(fileMD5, previewSize, seekPercentage)
|
||||
if err = service.fileCache.Store(ctx, cacheKey, imageBytes); err != nil {
|
||||
logger.Errorf("failed to cache HEIC image: %v", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
// For regular images that were already resized, cache and return
|
||||
if previewType == previewTypeImage {
|
||||
} else if strings.HasPrefix(file.Type, "image") {
|
||||
// Stream from file instead of os.ReadFile so we don't load every image fully into memory.
|
||||
// ResizeWithSize() holds the imaging semaphore and only reads in chunks / decodes one at a time.
|
||||
f, openErr := os.Open(file.RealPath)
|
||||
if openErr != nil {
|
||||
logger.Errorf("Failed to open image file '%s' (path: %s): %v", file.Name, file.RealPath, openErr)
|
||||
return nil, fmt.Errorf("failed to open image file: %w", openErr)
|
||||
}
|
||||
defer f.Close()
|
||||
imageBytes, err = service.CreatePreviewFromReaderWithSize(f, previewSize, file.Size)
|
||||
if err != nil {
|
||||
// Check if this is an unsupported JPEG format and FFmpeg fallback is enabled
|
||||
errMsg := err.Error()
|
||||
if strings.HasPrefix(file.Type, "image/jpeg") &&
|
||||
(strings.Contains(errMsg, "unrecognised marker") ||
|
||||
strings.Contains(errMsg, "invalid JPEG format") ||
|
||||
strings.Contains(errMsg, "Huffman")) {
|
||||
enableJPEGFallback := *settings.Config.Integrations.Media.Convert.ImagePreview[settings.JPEGImagePreview]
|
||||
if enableJPEGFallback && service.ffmpegService != nil {
|
||||
logger.Debugf("JPEG decode failed for '%s', falling back to FFmpeg: %v", file.Name, err)
|
||||
imageBytes, err = service.convertImageWithFFmpeg(ctx, file.RealPath, previewSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resize preview image with FFmpeg fallback: %w", err)
|
||||
}
|
||||
cacheKey := CacheKey(fileMD5, previewSize, seekPercentage)
|
||||
if err = service.fileCache.Store(ctx, cacheKey, imageBytes); err != nil {
|
||||
logger.Errorf("failed to cache FFmpeg-converted image: %v", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
if !enableJPEGFallback {
|
||||
return nil, fmt.Errorf("failed to resize preview image (unsupported JPEG format, FFmpeg conversion disabled in settings): %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to resize preview image (unsupported JPEG format, FFmpeg not available): %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to create image preview: %w", err)
|
||||
}
|
||||
if len(imageBytes) < 100 {
|
||||
logger.Errorf("Generated image too small for '%s' (type: %s): %d bytes", file.Name, file.Type, len(imageBytes))
|
||||
return nil, fmt.Errorf("generated image is too small, likely an error occurred: %d bytes", len(imageBytes))
|
||||
}
|
||||
cacheKey := CacheKey(fileMD5, previewSize, seekPercentage)
|
||||
if err = service.fileCache.Store(ctx, cacheKey, imageBytes); err != nil {
|
||||
logger.Errorf("failed to cache image: %v", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
} else if strings.HasPrefix(file.Type, "video") {
|
||||
// Check if this video format is enabled for preview generation
|
||||
ext = strings.TrimPrefix(strings.ToLower(filepath.Ext(file.Name)), ".")
|
||||
if !settings.CanConvertVideo(ext) {
|
||||
return nil, fmt.Errorf("video preview generation is disabled for .%s files in settings", ext)
|
||||
}
|
||||
|
||||
videoSeekPercentage := seekPercentage
|
||||
if videoSeekPercentage == 0 {
|
||||
videoSeekPercentage = 10
|
||||
}
|
||||
imageBytes, err = service.GenerateVideoPreview(ctx, file.RealPath, videoSeekPercentage)
|
||||
if err != nil {
|
||||
// Don't log client cancellations as errors
|
||||
if ctx.Err() != context.Canceled {
|
||||
logger.Errorf("Video preview generation failed for '%s' (path: %s, seek: %d%%): %v",
|
||||
file.Name, file.RealPath, videoSeekPercentage, err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to create image for video file: %w", err)
|
||||
}
|
||||
} else if strings.HasPrefix(file.Type, "audio") {
|
||||
// Extract album artwork from audio files
|
||||
if file.Metadata != nil && file.Metadata.AlbumArt != "" {
|
||||
imageBytes, err = base64.StdEncoding.DecodeString(file.Metadata.AlbumArt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode album artwork: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported media type: %s", ext)
|
||||
}
|
||||
|
||||
// Check if context was cancelled during processing
|
||||
@@ -464,47 +523,62 @@ func GeneratePreviewWithMD5(ctx context.Context, file iteminfo.ExtendedFileInfo,
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// Validate generated image size
|
||||
if len(imageBytes) < 100 {
|
||||
logger.Errorf("Generated image too small for '%s' (type: %s): %d bytes - likely an error occurred",
|
||||
file.Name, file.Type, len(imageBytes))
|
||||
return nil, fmt.Errorf("generated image is too small, likely an error occurred: %d bytes", len(imageBytes))
|
||||
}
|
||||
|
||||
// Resize if needed (for videos, audio, documents, office files)
|
||||
if previewSize != "original" {
|
||||
options, err := getPreviewOptions(previewSize)
|
||||
// resize image
|
||||
resizedBytes, err := service.CreatePreview(imageBytes, previewSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check if this is an unsupported JPEG format (extended sequential, etc.)
|
||||
errMsg := err.Error()
|
||||
if strings.HasPrefix(file.Type, "image/jpeg") &&
|
||||
(strings.Contains(errMsg, "unrecognised marker") ||
|
||||
strings.Contains(errMsg, "invalid JPEG format") ||
|
||||
strings.Contains(errMsg, "Huffman")) {
|
||||
|
||||
resizedBytes, err := service.CreatePreview(bytes.NewReader(imageBytes), 0, options)
|
||||
if err != nil {
|
||||
// For JPEG files, try FFmpeg fallback if resize failed
|
||||
if strings.HasPrefix(file.Type, "image/jpeg") {
|
||||
resizedBytes, err = handleJPEGFallback(ctx, service, file, previewSize, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Check if JPEG FFmpeg conversion is enabled (guaranteed non-nil after defaults)
|
||||
enableJPEGFallback := *settings.Config.Integrations.Media.Convert.ImagePreview[settings.JPEGImagePreview]
|
||||
|
||||
// Only attempt FFmpeg fallback if it's enabled and FFmpeg service is available
|
||||
if enableJPEGFallback && service.ffmpegService != nil {
|
||||
// Fall back to FFmpeg for problematic JPEG files
|
||||
logger.Debugf("JPEG decode failed for '%s', falling back to FFmpeg: %v", file.Name, err)
|
||||
resizedBytes, err = service.convertImageWithFFmpeg(ctx, file.RealPath, previewSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resize preview image with FFmpeg fallback: %w", err)
|
||||
}
|
||||
// Cache and return the FFmpeg-converted image
|
||||
cacheKey := CacheKey(fileMD5, previewSize, seekPercentage)
|
||||
if err = service.fileCache.Store(ctx, cacheKey, resizedBytes); err != nil {
|
||||
logger.Errorf("failed to cache FFmpeg-converted image: %v", err)
|
||||
}
|
||||
return resizedBytes, nil
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to resize preview image: %w", err)
|
||||
// If FFmpeg fallback is disabled or not available, return the original error
|
||||
if !enableJPEGFallback {
|
||||
return nil, fmt.Errorf("failed to resize preview image (unsupported JPEG format, FFmpeg conversion disabled in settings): %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to resize preview image (unsupported JPEG format, FFmpeg not available): %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to resize preview image: %w", err)
|
||||
}
|
||||
|
||||
// Cache and return resized image
|
||||
// Cache and return
|
||||
cacheKey := CacheKey(fileMD5, previewSize, seekPercentage)
|
||||
if err := service.fileCache.Store(ctx, cacheKey, resizedBytes); err != nil {
|
||||
logger.Errorf("failed to cache resized image: %v", err)
|
||||
}
|
||||
return resizedBytes, nil
|
||||
} else {
|
||||
cacheKey := CacheKey(fileMD5, previewSize, seekPercentage)
|
||||
if err := service.fileCache.Store(ctx, cacheKey, imageBytes); err != nil {
|
||||
logger.Errorf("failed to cache original image: %v", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
// Cache and return original size
|
||||
cacheKey := CacheKey(fileMD5, previewSize, seekPercentage)
|
||||
if err := service.fileCache.Store(ctx, cacheKey, imageBytes); err != nil {
|
||||
logger.Errorf("failed to cache original image: %v", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
func GeneratePreview(ctx context.Context, file iteminfo.ExtendedFileInfo, previewSize, officeUrl string, seekPercentage int) ([]byte, error) {
|
||||
@@ -517,40 +591,41 @@ func GeneratePreview(ctx context.Context, file iteminfo.ExtendedFileInfo, previe
|
||||
return GeneratePreviewWithMD5(ctx, file, previewSize, officeUrl, seekPercentage, cacheHash)
|
||||
}
|
||||
|
||||
// getPreviewOptions returns resize options for the given preview size
|
||||
func getPreviewOptions(previewSize string) (ResizeOptions, error) {
|
||||
func (s *Service) CreatePreview(data []byte, previewSize string) ([]byte, error) {
|
||||
return s.CreatePreviewFromReader(bytes.NewReader(data), previewSize)
|
||||
}
|
||||
|
||||
// CreatePreviewFromReader resizes an image from a reader (e.g. *os.File) without loading it fully into memory.
|
||||
// Used for the image preview path so only concurrencyLimit decodes run at once.
|
||||
func (s *Service) CreatePreviewFromReader(reader io.Reader, previewSize string) ([]byte, error) {
|
||||
return s.CreatePreviewFromReaderWithSize(reader, previewSize, 0)
|
||||
}
|
||||
|
||||
// CreatePreviewFromReaderWithSize resizes an image with file size information for semaphore selection.
|
||||
func (s *Service) CreatePreviewFromReaderWithSize(reader io.Reader, previewSize string, fileSize int64) ([]byte, error) {
|
||||
var options ResizeOptions
|
||||
|
||||
switch previewSize {
|
||||
case "xlarge":
|
||||
return ResizeOptions{
|
||||
Width: 1024,
|
||||
Height: 1024,
|
||||
ResizeMode: ResizeModeFit,
|
||||
Quality: QualityHigh,
|
||||
Format: FormatJpeg,
|
||||
}, nil
|
||||
case "large":
|
||||
return ResizeOptions{
|
||||
options = ResizeOptions{
|
||||
Width: 640,
|
||||
Height: 640,
|
||||
ResizeMode: ResizeModeFit,
|
||||
Quality: QualityHigh,
|
||||
Format: FormatJpeg,
|
||||
}, nil
|
||||
}
|
||||
case "small":
|
||||
return ResizeOptions{
|
||||
options = ResizeOptions{
|
||||
Width: 256,
|
||||
Height: 256,
|
||||
ResizeMode: ResizeModeFit,
|
||||
Quality: QualityMedium,
|
||||
Format: FormatJpeg,
|
||||
}, nil
|
||||
}
|
||||
default:
|
||||
return ResizeOptions{}, ErrUnsupportedFormat
|
||||
return nil, ErrUnsupportedFormat
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePreview resizes an image from a reader with the given options
|
||||
func (s *Service) CreatePreview(reader io.Reader, fileSize int64, options ResizeOptions) ([]byte, error) {
|
||||
output := &bytes.Buffer{}
|
||||
if err := s.ResizeWithSize(reader, output, fileSize, options); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1105,17 +1105,17 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/media/subtitles": {
|
||||
"get": {
|
||||
"description": "Returns raw subtitle content from external files or embedded streams",
|
||||
"description": "Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
"text/vtt"
|
||||
],
|
||||
"tags": [
|
||||
"Subtitles"
|
||||
],
|
||||
"summary": "Get subtitle content",
|
||||
"summary": "Extract embedded subtitles",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -1132,22 +1132,15 @@ const docTemplate = `{
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Subtitle track name (filename for external, descriptive name for embedded)",
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Whether this is an embedded stream (true) or external file (false), defaults to false",
|
||||
"name": "embedded",
|
||||
"type": "integer",
|
||||
"description": "Stream index for embedded subtitle extraction, defaults to 0",
|
||||
"name": "index",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Raw subtitle content in original format",
|
||||
"description": "Raw WebVTT subtitle content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1571,7 +1564,7 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "url encoded destination path where to place the files inside the destination source",
|
||||
"description": "url encoded destination path where to place the files inside the destination source, a directory must end in / to create a directory",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
@@ -3070,8 +3063,8 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "path within the share to upload to. Must be relative to share root.",
|
||||
"name": "path",
|
||||
"description": "Target path within the share to upload to. Must be relative to share root.",
|
||||
"name": "targetPath",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
@@ -3319,14 +3312,14 @@ const docTemplate = `{
|
||||
},
|
||||
"/public/api/share/image": {
|
||||
"get": {
|
||||
"description": "Returns a resizable preview (large size) for the banner or favicon file of a share",
|
||||
"description": "Serves the banner or favicon file for a share",
|
||||
"produces": [
|
||||
"image/jpeg"
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"Public Shares"
|
||||
],
|
||||
"summary": "Get share image (banner or favicon) as preview",
|
||||
"summary": "Get share image (banner or favicon)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -3350,7 +3343,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Preview image content (JPEG)",
|
||||
"description": "Image file content",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
@@ -3614,9 +3607,6 @@ const docTemplate = `{
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEdit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -3728,9 +3718,6 @@ const docTemplate = `{
|
||||
"description": "Override embedded field to show source name",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4088,11 +4075,8 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"albumArt": {
|
||||
"description": "album art image data (automatically base64-encoded in JSON)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
"description": "base64 encoded album art / video thumbnail",
|
||||
"type": "string"
|
||||
},
|
||||
"artist": {
|
||||
"description": "track artist",
|
||||
@@ -5068,9 +5052,6 @@ const docTemplate = `{
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEdit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5167,9 +5148,6 @@ const docTemplate = `{
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5212,9 +5190,6 @@ const docTemplate = `{
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEdit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5320,9 +5295,6 @@ const docTemplate = `{
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5368,9 +5340,6 @@ const docTemplate = `{
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEdit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5479,9 +5448,6 @@ const docTemplate = `{
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -1094,17 +1094,17 @@
|
||||
},
|
||||
"/api/media/subtitles": {
|
||||
"get": {
|
||||
"description": "Returns raw subtitle content from external files or embedded streams",
|
||||
"description": "Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
"text/vtt"
|
||||
],
|
||||
"tags": [
|
||||
"Subtitles"
|
||||
],
|
||||
"summary": "Get subtitle content",
|
||||
"summary": "Extract embedded subtitles",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -1121,22 +1121,15 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Subtitle track name (filename for external, descriptive name for embedded)",
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Whether this is an embedded stream (true) or external file (false), defaults to false",
|
||||
"name": "embedded",
|
||||
"type": "integer",
|
||||
"description": "Stream index for embedded subtitle extraction, defaults to 0",
|
||||
"name": "index",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Raw subtitle content in original format",
|
||||
"description": "Raw WebVTT subtitle content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1560,7 +1553,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "url encoded destination path where to place the files inside the destination source",
|
||||
"description": "url encoded destination path where to place the files inside the destination source, a directory must end in / to create a directory",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
@@ -3059,8 +3052,8 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "path within the share to upload to. Must be relative to share root.",
|
||||
"name": "path",
|
||||
"description": "Target path within the share to upload to. Must be relative to share root.",
|
||||
"name": "targetPath",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
@@ -3308,14 +3301,14 @@
|
||||
},
|
||||
"/public/api/share/image": {
|
||||
"get": {
|
||||
"description": "Returns a resizable preview (large size) for the banner or favicon file of a share",
|
||||
"description": "Serves the banner or favicon file for a share",
|
||||
"produces": [
|
||||
"image/jpeg"
|
||||
"application/octet-stream"
|
||||
],
|
||||
"tags": [
|
||||
"Public Shares"
|
||||
],
|
||||
"summary": "Get share image (banner or favicon) as preview",
|
||||
"summary": "Get share image (banner or favicon)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -3339,7 +3332,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Preview image content (JPEG)",
|
||||
"description": "Image file content",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
@@ -3603,9 +3596,6 @@
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEdit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -3717,9 +3707,6 @@
|
||||
"description": "Override embedded field to show source name",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -4077,11 +4064,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"albumArt": {
|
||||
"description": "album art image data (automatically base64-encoded in JSON)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
"description": "base64 encoded album art / video thumbnail",
|
||||
"type": "string"
|
||||
},
|
||||
"artist": {
|
||||
"description": "track artist",
|
||||
@@ -5057,9 +5041,6 @@
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEdit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5156,9 +5137,6 @@
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5201,9 +5179,6 @@
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEdit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5309,9 +5284,6 @@
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5357,9 +5329,6 @@
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"canEdit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5468,9 +5437,6 @@
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"sourceURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -122,8 +122,6 @@ definitions:
|
||||
type: string
|
||||
bannerUrl:
|
||||
type: string
|
||||
canEdit:
|
||||
type: boolean
|
||||
description:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
@@ -201,8 +199,6 @@ definitions:
|
||||
source:
|
||||
description: Override embedded field to show source name
|
||||
type: string
|
||||
sourceURL:
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
title:
|
||||
@@ -451,10 +447,8 @@ definitions:
|
||||
description: album name
|
||||
type: string
|
||||
albumArt:
|
||||
description: album art image data (automatically base64-encoded in JSON)
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
description: base64 encoded album art / video thumbnail
|
||||
type: string
|
||||
artist:
|
||||
description: track artist
|
||||
type: string
|
||||
@@ -1203,8 +1197,6 @@ definitions:
|
||||
type: string
|
||||
bannerUrl:
|
||||
type: string
|
||||
canEdit:
|
||||
type: boolean
|
||||
description:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
@@ -1272,8 +1264,6 @@ definitions:
|
||||
source:
|
||||
description: backend source is path to maintain between name changes
|
||||
type: string
|
||||
sourceURL:
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
title:
|
||||
@@ -1304,8 +1294,6 @@ definitions:
|
||||
type: string
|
||||
bannerUrl:
|
||||
type: string
|
||||
canEdit:
|
||||
type: boolean
|
||||
description:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
@@ -1379,8 +1367,6 @@ definitions:
|
||||
source:
|
||||
description: backend source is path to maintain between name changes
|
||||
type: string
|
||||
sourceURL:
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
title:
|
||||
@@ -1413,8 +1399,6 @@ definitions:
|
||||
type: string
|
||||
bannerUrl:
|
||||
type: string
|
||||
canEdit:
|
||||
type: boolean
|
||||
description:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
@@ -1490,8 +1474,6 @@ definitions:
|
||||
source:
|
||||
description: backend source is path to maintain between name changes
|
||||
type: string
|
||||
sourceURL:
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
title:
|
||||
@@ -2515,7 +2497,8 @@ paths:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns raw subtitle content from external files or embedded streams
|
||||
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
|
||||
@@ -2527,22 +2510,15 @@ paths:
|
||||
name: source
|
||||
required: true
|
||||
type: string
|
||||
- description: Subtitle track name (filename for external, descriptive name
|
||||
for embedded)
|
||||
- description: Stream index for embedded subtitle extraction, defaults to 0
|
||||
in: query
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: Whether this is an embedded stream (true) or external file (false),
|
||||
defaults to false
|
||||
in: query
|
||||
name: embedded
|
||||
type: boolean
|
||||
name: index
|
||||
type: integer
|
||||
produces:
|
||||
- text/plain
|
||||
- text/vtt
|
||||
responses:
|
||||
"200":
|
||||
description: Raw subtitle content in original format
|
||||
description: Raw WebVTT subtitle content
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
@@ -2569,7 +2545,7 @@ paths:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get subtitle content
|
||||
summary: Extract embedded subtitles
|
||||
tags:
|
||||
- Subtitles
|
||||
/api/preview:
|
||||
@@ -2879,7 +2855,7 @@ paths:
|
||||
Supports file uploads and directory creation.
|
||||
parameters:
|
||||
- description: url encoded destination path where to place the files inside
|
||||
the destination source
|
||||
the destination source, a directory must end in / to create a directory
|
||||
in: query
|
||||
name: path
|
||||
required: true
|
||||
@@ -3863,10 +3839,10 @@ paths:
|
||||
name: hash
|
||||
required: true
|
||||
type: string
|
||||
- description: path within the share to upload to. Must be relative to share
|
||||
root.
|
||||
- description: Target path within the share to upload to. Must be relative to
|
||||
share root.
|
||||
in: query
|
||||
name: path
|
||||
name: targetPath
|
||||
required: true
|
||||
type: string
|
||||
- description: If true, overwrite existing files/folders. Defaults to false.
|
||||
@@ -4042,8 +4018,7 @@ paths:
|
||||
- Public Shares
|
||||
/public/api/share/image:
|
||||
get:
|
||||
description: Returns a resizable preview (large size) for the banner or favicon
|
||||
file of a share
|
||||
description: Serves the banner or favicon file for a share
|
||||
parameters:
|
||||
- description: Share hash
|
||||
in: query
|
||||
@@ -4059,10 +4034,10 @@ paths:
|
||||
name: favicon
|
||||
type: boolean
|
||||
produces:
|
||||
- image/jpeg
|
||||
- application/octet-stream
|
||||
responses:
|
||||
"200":
|
||||
description: Preview image content (JPEG)
|
||||
description: Image file content
|
||||
schema:
|
||||
type: file
|
||||
"400":
|
||||
@@ -4083,7 +4058,7 @@ paths:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get share image (banner or favicon) as preview
|
||||
summary: Get share image (banner or favicon)
|
||||
tags:
|
||||
- Public Shares
|
||||
/public/api/shareinfo:
|
||||
|
||||
Generated
+1376
-393
File diff suppressed because it is too large.
Load diff
@@ -22,11 +22,12 @@
|
||||
"dependencies": {
|
||||
"@onlyoffice/document-editor-vue": "^1.4.0",
|
||||
"ace-builds": "^1.24.2",
|
||||
"axios": "^1.13.5",
|
||||
"axios": "^1.7.9",
|
||||
"clipboard": "^2.0.4",
|
||||
"css-vars-ponyfill": "^2.4.3",
|
||||
"dompurify": "^3.2.4",
|
||||
"epubjs": "^0.3.93",
|
||||
"file-loader": "^6.2.0",
|
||||
"glob": "^9.3.5",
|
||||
"highlight.js": "^11.11.1",
|
||||
"mammoth": "^1.9.1",
|
||||
@@ -40,9 +41,6 @@
|
||||
"vue-lazyload": "^3.0.0",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": "^10.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<title>"{{ .htmlVars.title }}"</title>
|
||||
<title>{{ .htmlVars.title }}</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="{{ .htmlVars.favicon }}">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<link rel="preload" href="{{ .htmlVars.staticURL }}/fonts/material-symbols-outlined.woff2" as="font" type="font/woff2" crossorigin>
|
||||
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
<link rel="manifest" href="{{ .htmlVars.manifestURL }}" crossorigin="use-credentials">
|
||||
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="{{ .htmlVars.color }}">
|
||||
|
||||
<!-- Add to home screen for Safari on iOS/iPadOS -->
|
||||
@@ -52,6 +52,40 @@
|
||||
<!-- Injected variables come from backend server -->
|
||||
<script>
|
||||
window.globalVars = {{ .globalVars }};
|
||||
var dynamicManifest = {
|
||||
"name": window.globalVars.name || 'FileBrowser Quantum',
|
||||
"short_name": window.globalVars.name || 'FileBrowser',
|
||||
"icons": [
|
||||
{
|
||||
"src": "{{ .htmlVars.pwaIcon192 }}",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "{{ .htmlVars.pwaIcon256 }}",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "{{ .htmlVars.pwaIcon512 }}",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
],
|
||||
"start_url": window.location.origin + "{{ .htmlVars.baseURL }}",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "{{ .htmlVars.color }}",
|
||||
"description": "{{ .htmlVars.description }}"
|
||||
}
|
||||
|
||||
const stringManifest = JSON.stringify(dynamicManifest);
|
||||
const blob = new Blob([stringManifest], { type: 'application/json' });
|
||||
const manifestURL = URL.createObjectURL(blob);
|
||||
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { globalVars } from "@/utils/constants";
|
||||
|
||||
const ssl = window.location.protocol === "https:";
|
||||
const protocol = ssl ? "wss:" : "ws:";
|
||||
|
||||
export default function command(url, command, onmessage, onclose) {
|
||||
url = `${protocol}//${window.location.host}${globalVars.baseURL}api/command${url}`;
|
||||
let conn = new window.WebSocket(url);
|
||||
conn.onopen = () => conn.send(command);
|
||||
conn.onmessage = onmessage;
|
||||
conn.onclose = onclose;
|
||||
}
|
||||
+52
-64
@@ -12,8 +12,8 @@ export async function fetchFiles(source, path, content = false, metadata = false
|
||||
}
|
||||
try {
|
||||
const apiPath = getApiPath('api/resources', {
|
||||
path: path,
|
||||
source: source,
|
||||
path: encodeURIComponent(path),
|
||||
source: encodeURIComponent(source),
|
||||
...(content && { content: 'true' }),
|
||||
...(metadata && { metadata: 'true' })
|
||||
})
|
||||
@@ -32,6 +32,8 @@ async function resourceAction(source, path, method, content) {
|
||||
throw new Error('no source provided')
|
||||
}
|
||||
try {
|
||||
source = encodeURIComponent(source)
|
||||
path = encodeURIComponent(path)
|
||||
const apiPath = getApiPath('api/resources', { path, source })
|
||||
let opts = { method }
|
||||
if (content) {
|
||||
@@ -74,12 +76,15 @@ export async function bulkDelete(items) {
|
||||
},
|
||||
body: JSON.stringify(items),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// 200 = all succeeded, 207 = partial success (some succeeded, some failed)
|
||||
// Both are valid responses that should be returned, not thrown as errors
|
||||
if (response.status === 200 || response.status === 207) {
|
||||
return data
|
||||
}
|
||||
|
||||
// For other error status codes, throw an error
|
||||
const error = new Error(data.message || response.statusText)
|
||||
error.status = response.status
|
||||
@@ -105,12 +110,12 @@ export async function download(format, files, shareHash = "") {
|
||||
// Check if chunked download should be used (single file only)
|
||||
const downloadChunkSizeMb = state.user?.fileLoading?.downloadChunkSizeMb || 0
|
||||
const sizeThreshold = downloadChunkSizeMb * 1024 * 1024;
|
||||
|
||||
const useChunkedDownload =
|
||||
downloadChunkSizeMb > 0 &&
|
||||
files.length === 1 &&
|
||||
!files[0].isDir &&
|
||||
files[0].size &&
|
||||
|
||||
const useChunkedDownload =
|
||||
downloadChunkSizeMb > 0 &&
|
||||
files.length === 1 &&
|
||||
!files[0].isDir &&
|
||||
files[0].size &&
|
||||
files[0].size >= sizeThreshold
|
||||
|
||||
if (useChunkedDownload) {
|
||||
@@ -122,14 +127,14 @@ export async function download(format, files, shareHash = "") {
|
||||
if (format !== 'zip') {
|
||||
format = 'tar.gz'
|
||||
}
|
||||
|
||||
|
||||
// For non-share downloads, validate single source and build file list
|
||||
let source = null
|
||||
let filePaths = []
|
||||
|
||||
|
||||
if (shareHash) {
|
||||
// For shares, no source parameter needed, just paths
|
||||
filePaths = files.map(file => file.path)
|
||||
filePaths = files.map(file => encodeURIComponent(file.path))
|
||||
} else {
|
||||
// Validate all files are from the same source
|
||||
for (let file of files) {
|
||||
@@ -141,19 +146,19 @@ export async function download(format, files, shareHash = "") {
|
||||
} else if (source !== file.source) {
|
||||
throw new Error('All files must be from the same source for downloads')
|
||||
}
|
||||
filePaths.push(file.path)
|
||||
filePaths.push(encodeURIComponent(file.path))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const params = {
|
||||
file: filePaths, // Array of file paths - getApiPath will create repeated parameters
|
||||
algo: format,
|
||||
...(shareHash && { hash: shareHash }),
|
||||
...(!shareHash && source && { source: source }),
|
||||
...(!shareHash && source && { source: encodeURIComponent(source) }),
|
||||
...(state.shareInfo.token && { token: state.shareInfo.token }),
|
||||
sessionId: state.sessionId
|
||||
}
|
||||
|
||||
|
||||
const apiPath = getApiPath(shareHash == "" ? 'api/raw' : 'public/api/raw', params)
|
||||
const url = window.origin + apiPath
|
||||
|
||||
@@ -167,7 +172,7 @@ export async function download(format, files, shareHash = "") {
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
|
||||
// Clean up after a short delay
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(link)
|
||||
@@ -176,36 +181,36 @@ export async function download(format, files, shareHash = "") {
|
||||
|
||||
async function downloadChunked(file, shareHash = "") {
|
||||
const chunkSizeMb = state.user?.fileLoading?.downloadChunkSizeMb || 0
|
||||
|
||||
|
||||
if (chunkSizeMb === 0) {
|
||||
throw new Error("Chunked download is disabled (chunk size is 0)")
|
||||
}
|
||||
const chunkSize = chunkSizeMb * 1024 * 1024 // Convert MB to bytes
|
||||
const fileSize = file.size
|
||||
|
||||
|
||||
// Extract filename from path if name is not available
|
||||
const fileName = file.name || (file.path ? file.path.split('/').pop() : 'download')
|
||||
|
||||
// Add to download manager
|
||||
const downloadId = downloadManager.add(file, shareHash)
|
||||
|
||||
|
||||
downloadManager.setStatus(downloadId, "downloading")
|
||||
|
||||
|
||||
// Show download prompt if not already shown (it should already be shown by downloadFiles, but check to be safe)
|
||||
const hasDownloadPrompt = state.hovers && state.hovers.some(h => h.name === 'download');
|
||||
|
||||
|
||||
if (!hasDownloadPrompt) {
|
||||
mutations.showHover({ name: 'download' })
|
||||
}
|
||||
|
||||
const params = {
|
||||
file: file.path,
|
||||
file: encodeURIComponent(file.path),
|
||||
...(shareHash && { hash: shareHash }),
|
||||
...(!shareHash && file.source && { source: file.source }),
|
||||
...(!shareHash && file.source && { source: encodeURIComponent(file.source) }),
|
||||
...(state.shareInfo.token && { token: state.shareInfo.token }),
|
||||
sessionId: state.sessionId
|
||||
}
|
||||
|
||||
|
||||
const apiPath = getApiPath(shareHash == "" ? 'api/raw' : 'public/api/raw', params)
|
||||
const baseUrl = window.origin + apiPath
|
||||
|
||||
@@ -220,7 +225,7 @@ async function downloadChunked(file, shareHash = "") {
|
||||
let loaded = 0
|
||||
|
||||
while (offset < fileSize) {
|
||||
|
||||
|
||||
const download = downloadManager.findById(downloadId);
|
||||
if (download && download.status === "cancelled") {
|
||||
// Silently handle cancellation - don't throw error
|
||||
@@ -244,7 +249,7 @@ async function downloadChunked(file, shareHash = "") {
|
||||
|
||||
// Track progress within the chunk using ReadableStream
|
||||
const expectedChunkSize = end - offset + 1;
|
||||
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const chunkParts = [];
|
||||
let chunkLoaded = 0;
|
||||
@@ -259,14 +264,14 @@ async function downloadChunked(file, shareHash = "") {
|
||||
reading = false;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
chunkParts.push(value);
|
||||
chunkLoaded += value.length;
|
||||
|
||||
|
||||
// Calculate progress: only count up to expected chunk size to avoid over-counting
|
||||
const chunkProgress = Math.min(chunkLoaded, expectedChunkSize);
|
||||
const totalLoaded = offset + chunkProgress;
|
||||
|
||||
|
||||
// Update progress in real-time, but throttle updates for performance
|
||||
if (chunkLoaded - lastProgressUpdate >= progressUpdateInterval || chunkLoaded >= expectedChunkSize) {
|
||||
downloadManager.updateProgress(downloadId, totalLoaded, fileSize);
|
||||
@@ -290,19 +295,19 @@ async function downloadChunked(file, shareHash = "") {
|
||||
chunk.set(part, position);
|
||||
position += part.length;
|
||||
}
|
||||
|
||||
|
||||
// Only use the expected chunk size portion if server returned more (handles Range header issues)
|
||||
const chunkToUse = chunk.byteLength > expectedChunkSize
|
||||
? chunk.slice(0, expectedChunkSize).buffer
|
||||
const chunkToUse = chunk.byteLength > expectedChunkSize
|
||||
? chunk.slice(0, expectedChunkSize).buffer
|
||||
: chunk.buffer;
|
||||
|
||||
|
||||
chunks.push(chunkToUse)
|
||||
// Always use expected chunk size for progress to avoid double-counting
|
||||
loaded += expectedChunkSize
|
||||
|
||||
|
||||
// Final progress update for this chunk
|
||||
downloadManager.updateProgress(downloadId, loaded, fileSize)
|
||||
|
||||
|
||||
offset = end + 1
|
||||
}
|
||||
|
||||
@@ -355,8 +360,8 @@ export function post(
|
||||
}
|
||||
try {
|
||||
const apiPath = getApiPath("api/resources", {
|
||||
path: path,
|
||||
source: source,
|
||||
path: encodeURIComponent(path),
|
||||
source: encodeURIComponent(source),
|
||||
override: overwrite,
|
||||
...(isDir && { isDir: 'true' })
|
||||
});
|
||||
@@ -448,7 +453,7 @@ export async function moveCopy(
|
||||
'Content-Type': 'application/json',
|
||||
'sessionId': state.sessionId
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch(apiPath, {
|
||||
method: 'PATCH',
|
||||
headers: headers,
|
||||
@@ -491,8 +496,8 @@ export async function checksum(source, path, algo) {
|
||||
}
|
||||
try {
|
||||
const params = {
|
||||
path: path,
|
||||
source: source,
|
||||
path: encodeURIComponent(path),
|
||||
source: encodeURIComponent(source),
|
||||
checksum: algo
|
||||
}
|
||||
const apiPath = getApiPath('api/resources', params)
|
||||
@@ -511,8 +516,8 @@ export function getDownloadURL(source, path, inline, useExternal) {
|
||||
}
|
||||
try {
|
||||
const params = {
|
||||
source: source,
|
||||
file: path,
|
||||
source: encodeURIComponent(source),
|
||||
file: encodeURIComponent(path),
|
||||
...(inline && { inline: 'true' })
|
||||
}
|
||||
const apiPath = getApiPath('api/raw', params)
|
||||
@@ -532,9 +537,9 @@ export function getPreviewURL(source, path, modified) {
|
||||
}
|
||||
try {
|
||||
const params = {
|
||||
path: path,
|
||||
path: encodeURIComponent(path),
|
||||
key: Date.parse(modified), // Use modified date as cache key
|
||||
source: source,
|
||||
source: encodeURIComponent(source),
|
||||
inline: 'true'
|
||||
}
|
||||
const apiPath = getApiPath('api/preview', params)
|
||||
@@ -563,9 +568,9 @@ export async function sources() {
|
||||
|
||||
export async function GetOfficeConfig(req) {
|
||||
const params = {
|
||||
path: req.path,
|
||||
...(req.hash && { hash: req.hash }),
|
||||
...(req.source && { source: req.source })
|
||||
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) {
|
||||
@@ -573,21 +578,4 @@ export async function GetOfficeConfig(req) {
|
||||
}
|
||||
const res = await fetchURL(apiPath)
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
export async function getSubtitleContent(source, path, subtitleName, embedded = false) {
|
||||
try {
|
||||
const apiPath = getApiPath('api/media/subtitles', {
|
||||
source: source,
|
||||
path: path,
|
||||
name: subtitleName,
|
||||
embedded: embedded.toString()
|
||||
})
|
||||
const res = await fetchURL(apiPath)
|
||||
const content = await res.text()
|
||||
return content
|
||||
} catch (err) {
|
||||
notify.showError(err.message || `Error fetching subtitle ${subtitleName}`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
+15
-11
@@ -1,6 +1,6 @@
|
||||
import { adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getPublicApiPath } from "@/utils/url.js";
|
||||
import { getPublicApiPath, encodedPath } from "@/utils/url.js";
|
||||
import { state } from "@/store";
|
||||
|
||||
// ============================================================================
|
||||
@@ -17,6 +17,7 @@ import { state } from "@/store";
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function fetchPub(path, hash, password = "", content = false, metadata = false) {
|
||||
path = encodedPath(path);
|
||||
const params = {
|
||||
path: path,
|
||||
hash,
|
||||
@@ -27,7 +28,7 @@ export async function fetchPub(path, hash, password = "", content = false, metad
|
||||
const apiPath = getPublicApiPath("resources", params);
|
||||
const response = await fetch(apiPath, {
|
||||
headers: {
|
||||
"X-SHARE-PASSWORD": password || "",
|
||||
"X-SHARE-PASSWORD": password ? encodeURIComponent(password) : "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -60,8 +61,10 @@ export async function fetchPub(path, hash, password = "", content = false, metad
|
||||
export function getDownloadURL(share, files, inline=false) {
|
||||
// Handle array of files for repeated 'file' parameters
|
||||
const fileArray = Array.isArray(files) ? files : [files]
|
||||
const filePaths = fileArray.map(file => encodeURIComponent(file))
|
||||
|
||||
const params = {
|
||||
file: fileArray, // Array of file paths - getPublicApiPath will create repeated parameters
|
||||
file: filePaths, // Array of file paths - getPublicApiPath will create repeated parameters
|
||||
hash: share.hash,
|
||||
token: share.token,
|
||||
...(inline && { inline: 'true' })
|
||||
@@ -79,7 +82,7 @@ export function getDownloadURL(share, files, inline=false) {
|
||||
export function getPreviewURL(path, size) {
|
||||
try {
|
||||
const params = {
|
||||
path: path,
|
||||
path: encodeURIComponent(path),
|
||||
hash: state.shareInfo.hash,
|
||||
inline: 'true',
|
||||
// Only add size parameter if specified and not the default
|
||||
@@ -112,7 +115,7 @@ export function post(
|
||||
}
|
||||
try {
|
||||
const apiPath = getPublicApiPath("resources", {
|
||||
path: path,
|
||||
targetPath: encodeURIComponent(path),
|
||||
hash: hash,
|
||||
override: overwrite,
|
||||
...(isDir && { isDir: 'true' })
|
||||
@@ -153,13 +156,13 @@ export function post(
|
||||
// If parsing fails, use responseText or default message
|
||||
errorMessage = request.responseText || errorMessage;
|
||||
}
|
||||
|
||||
|
||||
const error = new Error(errorMessage);
|
||||
error.status = request.status;
|
||||
|
||||
|
||||
// Show notification for upload errors
|
||||
notify.showError(errorMessage);
|
||||
|
||||
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
@@ -169,7 +172,7 @@ export function post(
|
||||
notify.showError("Network error during upload");
|
||||
reject(error);
|
||||
};
|
||||
|
||||
|
||||
request.onabort = () => {
|
||||
const error = new Error("Upload aborted");
|
||||
notify.showError("Upload was aborted");
|
||||
@@ -204,6 +207,7 @@ async function resourceAction(hash, path, method, content, token = "") {
|
||||
if (sharePassword) {
|
||||
headers["X-SHARE-PASSWORD"] = sharePassword;
|
||||
}
|
||||
path = encodeURIComponent(path)
|
||||
const apiPath = getPublicApiPath('resources', { path, hash: hash, token: token })
|
||||
const response = await fetch(apiPath, {
|
||||
method,
|
||||
@@ -236,12 +240,12 @@ export async function bulkDelete(items) {
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
throw new Error('items array is required and must not be empty')
|
||||
}
|
||||
|
||||
|
||||
const hash = state.shareInfo?.hash;
|
||||
if (!hash) {
|
||||
throw new Error('share hash is required')
|
||||
}
|
||||
|
||||
|
||||
const params = {
|
||||
hash: hash,
|
||||
...(state.shareInfo.token && { token: state.shareInfo.token }),
|
||||
|
||||
@@ -4,12 +4,14 @@ import { getApiPath } from "@/utils/url.js";
|
||||
|
||||
export default async function search(base, sources, query, largest = false) {
|
||||
try {
|
||||
query = encodeURIComponent(query);
|
||||
|
||||
// Ensure sources is an array
|
||||
const sourcesArray = Array.isArray(sources) ? sources : [sources];
|
||||
|
||||
const params = {
|
||||
query: query,
|
||||
sources: sourcesArray.join(",")
|
||||
sources: encodeURIComponent(sourcesArray.join(","))
|
||||
};
|
||||
|
||||
// Only include scope if searching a single source
|
||||
@@ -18,7 +20,7 @@ export default async function search(base, sources, query, largest = false) {
|
||||
if (!base.endsWith("/")) {
|
||||
base += "/";
|
||||
}
|
||||
params.scope = base;
|
||||
params.scope = encodeURIComponent(base);
|
||||
}
|
||||
|
||||
if (largest) {
|
||||
@@ -42,8 +44,8 @@ export async function findDuplicates(base, source, minSizeMb, useChecksum = fals
|
||||
base += "/";
|
||||
}
|
||||
const params = {
|
||||
scope: base,
|
||||
source: source,
|
||||
scope: encodeURIComponent(base),
|
||||
source: encodeURIComponent(source),
|
||||
minSizeMb: minSizeMb.toString()
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function list() {
|
||||
*/
|
||||
export async function get(path, source) {
|
||||
try {
|
||||
const params = { path, source };
|
||||
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
|
||||
const apiPath = getApiPath("api/share", params);
|
||||
let data = await fetchJSON(apiPath);
|
||||
return adjustedData(data);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { fetchURL, fetchJSON } from '@/api/utils'
|
||||
import { getApiPath, getPublicApiPath } from '@/utils/url.js'
|
||||
import { notify } from '@/notify' // Import notify for error handling
|
||||
|
||||
export async function getAllUsers() {
|
||||
try {
|
||||
const apiPath = getApiPath('api/users')
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<action v-if="showGoToRaw" icon="open_in_new" :label="$t('general.openFile')" @action="goToRaw()" />
|
||||
<action v-if="shouldShowParentFolder()" icon="folder" :label="$t('buttons.openParentFolder')" @action="openParentFolder" />
|
||||
<action v-if="isPreview && permissions.modify" icon="mode_edit" :label="$t('general.rename')" @action="showRenameHoverForPreview" />
|
||||
<action v-if="showWatch" icon="visibility" :label="$t('buttons.watchFile')" @action="watchFile()" />
|
||||
<action v-if="hasDownload && !req.isDir" icon="visibility" :label="$t('buttons.watchFile')" @action="watchFile()" />
|
||||
<action v-if="hasDownload" icon="file_download" :label="$t('general.download')" @action="startDownload" />
|
||||
<action v-if="showEdit" icon="edit" :label="$t('general.edit')" @action="edit()" />
|
||||
<action v-if="showSave" icon="save" :label="$t('general.save')" @action="save()" />
|
||||
@@ -187,9 +187,6 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showWatch() {
|
||||
return this.hasDownload && !this.req.isDir && !this.isShare;
|
||||
},
|
||||
showGoToItem() {
|
||||
return this.isDuplicateFinder && this.selectedCount == 1;
|
||||
},
|
||||
|
||||
@@ -250,7 +250,7 @@ export default {
|
||||
let index = 0;
|
||||
|
||||
const updateThumbnail = () => {
|
||||
if (!state.popupPreviewSourceInfo || state.popupPreviewSourceInfo.path !== this.path) {
|
||||
if (state.popupPreviewSourceInfo?.url === "") {
|
||||
this.previewTimeouts.forEach(clearTimeout);
|
||||
this.previewTimeouts = [];
|
||||
return;
|
||||
@@ -361,11 +361,6 @@ export default {
|
||||
this.svgPath = result.svgPath || "";
|
||||
this.updateImageTargetSrc();
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up any pending animation timeouts
|
||||
this.previewTimeouts.forEach(clearTimeout);
|
||||
this.previewTimeouts = [];
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -406,10 +406,6 @@ export default {
|
||||
},
|
||||
/** @param {MouseEvent} event */
|
||||
onRightClick(event) {
|
||||
if (!this.updateGlobalState) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.preventDefault(); // Prevent default context menu
|
||||
// If one or fewer items are selected, reset the selection
|
||||
if (this.updateGlobalState) {
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
:name="getItemName(item.path)"
|
||||
:isDir="item.type === 'directory'"
|
||||
:source="item.source"
|
||||
:type="item.type"
|
||||
:size="item.size"
|
||||
:modified="item.modified"
|
||||
:type="item.type || 'file'"
|
||||
:size="item.size || 0"
|
||||
:modified="item.modified || ''"
|
||||
:index="index"
|
||||
:path="item.path"
|
||||
:hasPreview="item.hasPreview"
|
||||
@@ -57,7 +57,6 @@ import { notify } from "@/notify";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import ListingItem from "@/components/files/ListingItem.vue";
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
import url from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "delete",
|
||||
@@ -108,7 +107,6 @@ export default {
|
||||
size: item.size,
|
||||
modified: item.modified,
|
||||
previewUrl: previewUrl,
|
||||
hasPreview: item.hasPreview,
|
||||
});
|
||||
} else if (!this.isListing) {
|
||||
const item = state.req.items[state.selected[0]];
|
||||
@@ -120,7 +118,6 @@ export default {
|
||||
size: item.size,
|
||||
modified: item.modified,
|
||||
previewUrl: previewUrl,
|
||||
hasPreview: item.hasPreview,
|
||||
});
|
||||
} else {
|
||||
for (let index of state.selected) {
|
||||
@@ -133,7 +130,6 @@ export default {
|
||||
size: item.size,
|
||||
modified: item.modified,
|
||||
previewUrl: previewUrl,
|
||||
hasPreview: item.hasPreview,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -231,7 +227,7 @@ export default {
|
||||
const failedCount = response.failed ? response.failed.length : 0;
|
||||
|
||||
if (failedCount === 0) {
|
||||
// All succeeded - close prompt and handle navigation
|
||||
// All succeeded - close prompt and reload
|
||||
buttons.success("delete");
|
||||
notify.showSuccessToast(this.$t('prompts.deleted'));
|
||||
|
||||
@@ -246,27 +242,7 @@ export default {
|
||||
if (!this.items) {
|
||||
mutations.resetSelected();
|
||||
}
|
||||
|
||||
// Smart navigation: if in preview mode with single item deletion, navigate intelligently
|
||||
if (getters.isPreviewView() && this.itemsToDelete.length === 1) {
|
||||
// Try next, then previous, then parent directory
|
||||
if (state.navigation.nextItem) {
|
||||
// Use goToItem with undefined to use router.replace (no history entry)
|
||||
url.goToItem(state.navigation.nextItem.source, state.navigation.nextItem.path, undefined);
|
||||
} else if (state.navigation.previousItem) {
|
||||
// Use goToItem with undefined to use router.replace (no history entry)
|
||||
url.goToItem(state.navigation.previousItem.source, state.navigation.previousItem.path, undefined);
|
||||
} else {
|
||||
// Navigate to parent directory of deleted item
|
||||
const deletedItemPath = this.itemsToDelete[0].path;
|
||||
const parentPath = url.removeLastDir(deletedItemPath);
|
||||
const deletedItemSource = this.itemsToDelete[0].source;
|
||||
url.goToItem(deletedItemSource, parentPath, {});
|
||||
}
|
||||
} else {
|
||||
// For listing view or multiple deletions, just reload
|
||||
mutations.setReload(true);
|
||||
}
|
||||
mutations.setReload(true);
|
||||
} else if (succeededCount > 0) {
|
||||
// Partial success - keep prompt open to show errors
|
||||
buttons.done("delete");
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
:modified="item.originalItem?.modified || new Date().toISOString()"
|
||||
:index="index"
|
||||
:path="item.path"
|
||||
:hasPreview="item.originalItem?.hasPreview && item.type !== 'directory' || false"
|
||||
:hasPreview="item.originalItem?.hasPreview || false"
|
||||
:metadata="item.originalItem?.metadata"
|
||||
:hasDuration="item.originalItem?.hasDuration || false"
|
||||
:updateGlobalState="false"
|
||||
@@ -412,14 +412,13 @@ export default {
|
||||
handleItemClick(item, _index, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Display mode: single click navigates directly to the file
|
||||
|
||||
if (this.isDisplayMode) {
|
||||
this.navigateToItem(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// Browse mode: single click selects only, emit selected, do not navigate
|
||||
// Create a synthetic event-like object for compatibility with existing methods
|
||||
const syntheticEvent = {
|
||||
currentTarget: {
|
||||
dataset: {
|
||||
@@ -429,17 +428,18 @@ export default {
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
};
|
||||
this.select(syntheticEvent);
|
||||
|
||||
if (state.user.singleClick) {
|
||||
this.next(syntheticEvent);
|
||||
} else {
|
||||
this.select(syntheticEvent);
|
||||
}
|
||||
},
|
||||
handleItemDblClick(item, _index, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Double click: navigate (into folder or open file)
|
||||
if (this.isDisplayMode) {
|
||||
this.navigateToItem(item);
|
||||
return;
|
||||
}
|
||||
// Create a synthetic event for double-click
|
||||
const syntheticEvent = {
|
||||
currentTarget: {
|
||||
dataset: {
|
||||
@@ -457,12 +457,9 @@ export default {
|
||||
if (this.selected === path) {
|
||||
this.selected = null;
|
||||
this.selectedSource = null;
|
||||
this.selectedType = null;
|
||||
this.$emit("update:selected", {
|
||||
path: this.current,
|
||||
source: this.source,
|
||||
type: 'directory',
|
||||
isValid: !this.requireFileSelection,
|
||||
source: this.source
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -471,12 +468,9 @@ export default {
|
||||
this.selected = path;
|
||||
let clickedItem = this.items.find(item => item.path === path);
|
||||
this.selectedSource = clickedItem ? clickedItem.source : this.source;
|
||||
this.selectedType = clickedItem ? clickedItem.type : null;
|
||||
this.$emit("update:selected", {
|
||||
path: this.selected,
|
||||
source: this.selectedSource,
|
||||
type: this.selectedType,
|
||||
isValid: this.selectedType === 'directory' ? !this.requireFileSelection : true,
|
||||
source: this.selectedSource
|
||||
});
|
||||
},
|
||||
createDir: async function () {
|
||||
|
||||
@@ -27,20 +27,31 @@
|
||||
</div>
|
||||
|
||||
<div v-if="isPickingFile">
|
||||
<file-list @update:selected="updateTempFilePath" :browse-source="displaySource" :browse-path="displayPath"
|
||||
:show-files="true" :show-folders="true" :allowed-file-types="filePickerAllowedTypes"
|
||||
:require-file-selection="true" :title="filePickerTitle">
|
||||
<file-list
|
||||
@update:selected="updateTempFilePath"
|
||||
:browse-source="displaySource"
|
||||
:browse-path="displayPath"
|
||||
:show-files="true"
|
||||
:show-folders="true"
|
||||
:allowed-file-types="filePickerAllowedTypes"
|
||||
:require-file-selection="true"
|
||||
:title="filePickerTitle">
|
||||
</file-list>
|
||||
<div class="card-action">
|
||||
<button class="button button--flat" @click="cancelFilePicker" :aria-label="$t('general.cancel')"
|
||||
:title="$t('general.cancel')">
|
||||
{{ $t("general.cancel") }}
|
||||
</button>
|
||||
<button class="button button--flat button--blue" @click="confirmFilePicker" :disabled="!filePickerSelectionValid" :aria-label="$t('general.ok')"
|
||||
:title="$t('general.ok')">
|
||||
{{ $t("general.ok") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isPickingFile">
|
||||
<div v-if="isFiles" aria-label="share-path" class="searchContext button"> {{ $t('general.path', { suffix: ':' }) }} {{
|
||||
<div aria-label="share-path" class="searchContext button"> {{ $t('general.path', { suffix: ':' }) }} {{
|
||||
displayPath }}</div>
|
||||
<div v-else>
|
||||
<a :href="link.sourceURL" class="button button--block">
|
||||
{{ $t('buttons.goToSource') }}
|
||||
</a>
|
||||
</div>
|
||||
<p> {{ $t('share.notice') }} </p>
|
||||
|
||||
<div v-if="listing">
|
||||
@@ -99,8 +110,8 @@
|
||||
</i>
|
||||
</p>
|
||||
<div class="form-flex-group">
|
||||
<input class="form-grow input flat-right" v-focus type="number" max="2147483647" min="0"
|
||||
@keyup.enter="submit" v-model.trim="time" />
|
||||
<input class="form-grow input flat-right" v-focus type="number" max="2147483647" min="0" @keyup.enter="submit"
|
||||
v-model.trim="time" />
|
||||
<select class="flat-left input form-dropdown" v-model="unit" :aria-label="$t('time.unit')">
|
||||
<option value="minutes">{{ $t("time.minutes") }}</option>
|
||||
<option value="hours">{{ $t("time.hours") }}</option>
|
||||
@@ -137,19 +148,16 @@
|
||||
{{ $t('share.customizeSidebarLinksButton') }}
|
||||
</button>
|
||||
<div class="settings-items" style="margin-top: 0.5em;">
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowModify"
|
||||
:name="$t('share.allowModify')" :description="$t('share.allowModifyDescription')"
|
||||
aria-label="allow editing files toggle" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowCreate"
|
||||
:name="$t('share.allowCreate')" :description="$t('share.allowCreateDescription')"
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowModify" :name="$t('share.allowModify')"
|
||||
:description="$t('share.allowModifyDescription')" aria-label="allow editing files toggle" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowCreate" :name="$t('share.allowCreate')"
|
||||
:description="$t('share.allowCreateDescription')"
|
||||
aria-label="allow creating and uploading files and folders toggle" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowDelete"
|
||||
:name="$t('share.allowDelete')" :description="$t('share.allowDeleteDescription')"
|
||||
aria-label="allow deleting files toggle" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowDelete" :name="$t('share.allowDelete')"
|
||||
:description="$t('share.allowDeleteDescription')" aria-label="allow deleting files toggle" />
|
||||
</div>
|
||||
</div>
|
||||
<SettingsItem :title="showMoreExpanded ? $t('buttons.showLess') : $t('buttons.showMore')" :collapsable="true"
|
||||
:start-collapsed="!showMoreExpanded" @toggle="showMoreExpanded = $event">
|
||||
<SettingsItem :title="showMoreExpanded ? $t('buttons.showLess') : $t('buttons.showMore')" :collapsable="true" :start-collapsed="!showMoreExpanded" @toggle="showMoreExpanded = $event">
|
||||
<div class="settings-items">
|
||||
<p>
|
||||
{{ $t("prompts.shareTheme") }}
|
||||
@@ -205,8 +213,7 @@
|
||||
<p>
|
||||
{{ $t("share.enforceDarkLightMode") }}
|
||||
<i class="no-select material-symbols-outlined tooltip-info-icon"
|
||||
@mouseenter="showTooltip($event, $t('share.enforceDarkLightModeDescription'))"
|
||||
@mouseleave="hideTooltip">
|
||||
@mouseenter="showTooltip($event, $t('share.enforceDarkLightModeDescription'))" @mouseleave="hideTooltip">
|
||||
help
|
||||
</i>
|
||||
</p>
|
||||
@@ -220,22 +227,22 @@
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="disableThumbnails"
|
||||
:name="$t('share.disableThumbnails')" :description="$t('share.disableThumbnailsDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="showHidden"
|
||||
:name="$t('profileSettings.showHiddenFiles')"
|
||||
:description="$t('profileSettings.showHiddenFilesDescription')" />
|
||||
<ToggleSwitch v-if="shareType !== 'upload'" class="item" v-model="disableNavButtons"
|
||||
:name="$t('share.hideNavButtons')" :description="$t('share.hideNavButtonsDescription')" />
|
||||
:name="$t('profileSettings.showHiddenFiles')" :description="$t('profileSettings.showHiddenFilesDescription')" />
|
||||
<ToggleSwitch v-if="shareType !== 'upload'" class="item" v-model="disableNavButtons" :name="$t('share.hideNavButtons')"
|
||||
:description="$t('share.hideNavButtonsDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableShareCard" :name="$t('share.disableShareCard')"
|
||||
:description="$t('share.disableShareCardDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableSidebar" :name="$t('share.disableSidebar')"
|
||||
:description="$t('share.disableSidebarDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableSidebar"
|
||||
:name="$t('share.disableSidebar')" :description="$t('share.disableSidebarDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="perUserDownloadLimit"
|
||||
:name="$t('share.perUserDownloadLimit')" :description="$t('share.perUserDownloadLimitDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="extractEmbeddedSubtitles"
|
||||
:name="$t('share.extractEmbeddedSubtitles')"
|
||||
:description="$t('share.extractEmbeddedSubtitlesDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableLoginOption" :name="$t('share.disableLoginOption')"
|
||||
:description="$t('share.disableLoginOptionDescription')" />
|
||||
</div>
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="extractEmbeddedSubtitles"
|
||||
:name="$t('share.extractEmbeddedSubtitles')"
|
||||
:description="$t('share.extractEmbeddedSubtitlesDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableLoginOption"
|
||||
:name="$t('share.disableLoginOption')"
|
||||
:description="$t('share.disableLoginOptionDescription')" />
|
||||
</div>
|
||||
|
||||
<div v-if="shareType === 'normal'">
|
||||
<p>
|
||||
@@ -335,16 +342,6 @@
|
||||
{{ $t("general.share") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isPickingFile" class="card-action">
|
||||
<button class="button button--flat" @click="cancelFilePicker" :aria-label="$t('general.cancel')"
|
||||
:title="$t('general.cancel')">
|
||||
{{ $t("general.cancel") }}
|
||||
</button>
|
||||
<button class="button button--flat button--blue" @click="confirmFilePicker" :disabled="!filePickerSelectionValid"
|
||||
:aria-label="$t('general.ok')" :title="$t('general.ok')">
|
||||
{{ $t("general.ok") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { notify } from "@/notify";
|
||||
@@ -479,9 +476,6 @@ export default {
|
||||
isListing() {
|
||||
return getters.isListing(); // Access getter directly from the store
|
||||
},
|
||||
isFiles() {
|
||||
return getters.isFiles(); // Access getter directly from the store
|
||||
},
|
||||
url() {
|
||||
if (state.isSearchActive) {
|
||||
const file = /** @type {FilebrowserFile} */ (this.selected[0]);
|
||||
@@ -870,7 +864,7 @@ export default {
|
||||
icon: "qr_code"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
// Only add Download link for normal shares, not upload shares
|
||||
if (this.shareType !== 'upload') {
|
||||
this.sidebarLinks.push({
|
||||
@@ -970,7 +964,7 @@ export default {
|
||||
if (this.tempFilePath && this.tempFileSource) {
|
||||
// Build query params format: source=sourcename&path=indexpath
|
||||
const queryParams = `source=${encodeURIComponent(this.tempFileSource)}&path=${encodeURIComponent(this.tempFilePath)}`;
|
||||
|
||||
|
||||
if (this.filePickerField === 'banner') {
|
||||
this.banner = queryParams;
|
||||
} else if (this.filePickerField === 'favicon') {
|
||||
|
||||
@@ -20,10 +20,7 @@
|
||||
class="link-drag-handle">
|
||||
<i class="material-icons">drag_indicator</i>
|
||||
</div>
|
||||
<div v-if="link.category === 'divider'" class="link-icon">
|
||||
<i class="material-icons">horizontal_rule</i>
|
||||
</div>
|
||||
<div v-else class="link-icon">
|
||||
<div class="link-icon">
|
||||
<i :class="getIconClass(link.icon)">{{ link.icon }}</i>
|
||||
</div>
|
||||
<div class="link-details">
|
||||
@@ -69,7 +66,6 @@
|
||||
<option value="share">{{ $t('general.share') }}</option>
|
||||
<option v-if="context === 'user'" value="tool">{{ $t('general.tool') }}</option>
|
||||
<option value="custom">{{ $t('sidebar.customLink') }}</option>
|
||||
<option value="divider">{{ $t('general.divider') }}</option>
|
||||
<option v-if="context === 'share'" value="shareInfo">{{ $t('share.shareInfo') }}</option>
|
||||
<option v-if="context === 'share'" value="download">{{ $t('general.download') }}</option>
|
||||
</select>
|
||||
@@ -152,13 +148,6 @@
|
||||
<p>{{ $t('share.downloadDescription') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Divider - special visual separator -->
|
||||
<div v-if="newLink.category === 'divider'" class="form-group">
|
||||
<p>{{ $t('sidebar.linkName') }}</p>
|
||||
<input aria-label="Link Name" v-model="newLink.name" type="text" class="input"
|
||||
:placeholder="$t('sidebar.dividerNamePlaceholder')" />
|
||||
</div>
|
||||
|
||||
<!-- Custom Link Input -->
|
||||
<div v-if="newLink.category === 'custom'" class="form-group">
|
||||
<p>{{ $t('sidebar.linkName') }}</p>
|
||||
@@ -170,8 +159,8 @@
|
||||
:placeholder="$t('sidebar.linkUrlPlaceholder')" />
|
||||
</div>
|
||||
|
||||
<!-- Icon Selection - Available for ALL link types except divider -->
|
||||
<div v-if="newLink.category && newLink.category !== 'divider'" class="form-group">
|
||||
<!-- Icon Selection - Available for ALL link types -->
|
||||
<div v-if="newLink.category" class="form-group">
|
||||
<p>{{ $t('sidebar.linkIcon') }}</p>
|
||||
<div class="icon-input-group">
|
||||
<input v-model="newLink.icon" type="text" class="input icon-input"
|
||||
@@ -296,10 +285,8 @@ export default {
|
||||
isNewLinkValid() {
|
||||
if (!this.newLink.category) return false;
|
||||
|
||||
// Special link types that don't need additional validation
|
||||
if (this.newLink.category === "shareInfo" ||
|
||||
this.newLink.category === "download" ||
|
||||
this.newLink.category === "divider") {
|
||||
// Special link types for shares don't need additional validation
|
||||
if (this.newLink.category === "shareInfo" || this.newLink.category === "download") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -405,16 +392,10 @@ export default {
|
||||
share: this.$t('general.share'),
|
||||
shareInfo: this.$t('share.shareInfo'),
|
||||
download: this.$t('general.download'),
|
||||
divider: this.$t('general.divider'),
|
||||
};
|
||||
return labels[category] || category;
|
||||
},
|
||||
getLinkDisplayName(link) {
|
||||
// For dividers, show the name or a default
|
||||
if (link.category === 'divider') {
|
||||
return link.name || this.$t('general.divider');
|
||||
}
|
||||
|
||||
// Check if the name looks like a translation key that needs translating
|
||||
if (link.category === 'shareInfo' && link.name === 'share.shareInfo') {
|
||||
return this.$t('share.shareInfo');
|
||||
@@ -565,11 +546,6 @@ export default {
|
||||
} else if (this.newLink.category === "download") {
|
||||
// Download is a special action link
|
||||
linkData.target = "#";
|
||||
} else if (this.newLink.category === "divider") {
|
||||
// Divider is a visual separator with no action
|
||||
linkData.target = "#";
|
||||
linkData.name = this.newLink.name || ""; // Keep the custom name if provided
|
||||
linkData.icon = ""; // Dividers don't need an icon
|
||||
} else if (this.newLink.category === "custom") {
|
||||
linkData.target = this.processCustomUrl(this.newLink.target);
|
||||
} else if (this.newLink.category === "source") {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { availableLocales } from "@/i18n/index.ts";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Languages",
|
||||
@@ -20,7 +19,32 @@ export default defineComponent({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
locales: availableLocales,
|
||||
locales: {
|
||||
he: "he",
|
||||
hu: "hu",
|
||||
ar: "ar",
|
||||
de: "de",
|
||||
en: "en",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
is: "is",
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
ko: "ko",
|
||||
nlBE: "nl-be",
|
||||
pl: "pl",
|
||||
ptBR: "pt-br",
|
||||
pt: "pt",
|
||||
ro: "ro",
|
||||
ru: "ru",
|
||||
sk: "sk",
|
||||
svSE: "sv-se",
|
||||
tr: "tr",
|
||||
ua: "ua",
|
||||
zhCN: "zh-cn",
|
||||
zhTW: "zh-tw",
|
||||
cz: "cz",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="!disableQuickToggles" class="card-wrapper" @mouseleave="hideTooltip">
|
||||
<div class="quick-toggles" :class="{ 'extra-padding': hasCreateOptions }">
|
||||
<div class="quick-toggles" :class="{ 'extra-padding': !hasCreateOptions }">
|
||||
<div class="clickable" :class="{ active: user?.singleClick }" @click="toggleClick"
|
||||
@mouseenter="showTooltip($event, $t('index.toggleClick'))" @mouseleave="hideTooltip" v-if="!isInvalidShare">
|
||||
<i class="material-icons">ads_click</i>
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
},
|
||||
hasCreateOptions() {
|
||||
if (getters.isShare()) {
|
||||
return state.shareInfo?.allowCreate == true
|
||||
return state.shareInfo?.allowCreate
|
||||
}
|
||||
return state.user?.permissions?.create || state.user?.permissions?.share || state.user?.permissions?.admin;
|
||||
},
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
shouldShowLogin() {
|
||||
if (getters.isShare()) {
|
||||
// Don't show login until shareInfo is fully loaded
|
||||
if (state.shareInfo && state.shareInfo?.disableLoginOption) {
|
||||
if (state.shareInfo && state.shareInfo.disableLoginOption == undefined) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -146,6 +146,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
openContextMenu() {
|
||||
mutations.resetSelected();
|
||||
mutations.showHover({
|
||||
|
||||
@@ -7,33 +7,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Links section header -->
|
||||
<div class="sidebar-links-header"
|
||||
:class="{ 'with-top-spacing': isShare && !disableShareCard }">
|
||||
<i :class="{ 'disabled': !isLoggedIn }" @click="goHome()" class="material-icons action">home</i>
|
||||
<div class="sidebar-links-header" :class="{ 'no-edit-options': isShare, 'with-top-spacing': isShare && !disableShareCard }">
|
||||
<i v-if="!isShare" @click="goHome()" class="material-icons action">home</i>
|
||||
<span>{{ $t("general.links") }}</span>
|
||||
<i v-if="isShare" aria-label="Edit Share" @mouseenter="showTooltip($event, editShareText())" @mouseleave="hideTooltip"
|
||||
:class="{ 'disabled': !canEdit }"
|
||||
@click="showEditShareHover" class="material-icons action">edit</i>
|
||||
<i v-else @mouseenter="showTooltip($event, $t('sidebar.customizeLinks'))" @mouseleave="hideTooltip"
|
||||
<i v-if="!isShare" @mouseenter="showTooltip($event, $t('sidebar.customizeLinks'))" @mouseleave="hideTooltip"
|
||||
@click="openSidebarLinksPrompt" class="material-icons action">edit</i>
|
||||
</div>
|
||||
|
||||
<transition-group name="expand" tag="div" class="inner-card">
|
||||
<template v-for="(link, index) in sidebarLinksToDisplay" :key="`link-${index}-${link.category}`">
|
||||
<!-- Divider: renders as hr line or text label -->
|
||||
<div v-if="link.category === 'divider'" class="sidebar-divider-container">
|
||||
<hr v-if="!link.name || link.name.toLowerCase() === 'divider'" class="sidebar-divider" />
|
||||
<span v-else class="sidebar-divider-text">{{ link.name }}</span>
|
||||
</div>
|
||||
<a v-else-if="link.category == 'custom' && link.name == 'sourceLocation'" :href="link.target"
|
||||
:aria-label="link.name" class="action button sidebar-link-button" @click.prevent="handleLinkClick(link)">
|
||||
<div class="link-container">
|
||||
<i class="material-icons link-icon">open_in_new</i>
|
||||
<span>{{ $t('buttons.goToSource') }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Source-type links: styled exactly like original sources -->
|
||||
<a v-else-if="link.category === 'source'" :href="getLinkHref(link)"
|
||||
<a v-if="link.category === 'source'" :href="getLinkHref(link)"
|
||||
class="action button source-button sidebar-link-button" :class="{
|
||||
active: isLinkActive(link),
|
||||
disabled: !isLinkAccessible(link)
|
||||
@@ -62,11 +46,12 @@
|
||||
</i>
|
||||
</div>
|
||||
<div v-if="hasUsageInfo(link)" class="usage-info">
|
||||
<ProgressBar
|
||||
<ProgressBar
|
||||
:key="`progress-${link.sourceName}-${sourceInfo[link.sourceName]?.used || 0}-${sourceInfo[link.sourceName]?.total || 0}`"
|
||||
:val="getProgressBarValue(sourceInfo[link.sourceName] || {})"
|
||||
:max="(sourceInfo[link.sourceName] || {}).total || 1"
|
||||
:status="getProgressBarStatus(sourceInfo[link.sourceName] || {})" unit="bytes">
|
||||
:val="getProgressBarValue(sourceInfo[link.sourceName] || {})"
|
||||
:max="(sourceInfo[link.sourceName] || {}).total || 1"
|
||||
:status="getProgressBarStatus(sourceInfo[link.sourceName] || {})"
|
||||
unit="bytes">
|
||||
</ProgressBar>
|
||||
</div>
|
||||
</a>
|
||||
@@ -74,12 +59,22 @@
|
||||
<!-- Non-source links: tool and custom links with simple icon style -->
|
||||
<a v-else :aria-label="link.name" :href="getLinkHref(link)" class="action button sidebar-link-button"
|
||||
:class="{ active: isLinkActive(link) }" @click.prevent="handleLinkClick(link)">
|
||||
<div class="link-container">
|
||||
<div class="link-container">
|
||||
<i :class="getIconClass(link.icon) + ' link-icon'">{{ link.icon }}</i>
|
||||
<span>{{ link.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- Edit Share link - only shown when viewing a share and user has share permissions -->
|
||||
<a v-if="isShare && canEditShare" :aria-label="$t('general.edit', { suffix: ' ' + $t('general.share') })" href="#"
|
||||
class="action button sidebar-link-button"
|
||||
@click.prevent="showEditShareHover">
|
||||
<div class="link-container">
|
||||
<i class="material-icons link-icon">edit</i>
|
||||
<span>{{ $t("general.edit", { suffix: " " + $t("general.share") }) }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</transition-group>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -102,17 +97,11 @@ export default {
|
||||
ShareInfo,
|
||||
},
|
||||
computed: {
|
||||
canEdit() {
|
||||
return state.shareInfo?.canEdit || false;
|
||||
},
|
||||
isLoggedIn() {
|
||||
return state.user?.username !== 'anonymous';
|
||||
},
|
||||
isShare: () => getters.isShare(),
|
||||
hasLinks() {
|
||||
return this.sidebarLinksToDisplay?.length > 0;
|
||||
},
|
||||
user: () => (state.user || { username: 'anonymous' }),
|
||||
user: () => (state.user || {username: 'anonymous'}),
|
||||
sourceInfo() {
|
||||
// Access state.sources.info to create reactive dependency
|
||||
return state.sources.info;
|
||||
@@ -128,6 +117,10 @@ export default {
|
||||
// Check if user has customized their links
|
||||
return this.user?.sidebarLinks && this.user.sidebarLinks.length > 0;
|
||||
},
|
||||
canEditShare() {
|
||||
// Check if user is logged in and has share permissions
|
||||
return state.user && state.user.permissions && state.user.permissions.share;
|
||||
},
|
||||
// Share info card props
|
||||
disableShareCard() {
|
||||
return state.shareInfo?.disableShareCard;
|
||||
@@ -158,9 +151,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getIconClass,
|
||||
editShareText() {
|
||||
return this.$t("general.edit") + " " + this.$t("general.share");
|
||||
},
|
||||
hasUsageInfo(link) {
|
||||
// Check if usage info should be displayed for this link
|
||||
// Returns true when link is accessible and has usage > 0
|
||||
@@ -195,9 +185,6 @@ export default {
|
||||
return baseURL + target;
|
||||
},
|
||||
goHome() {
|
||||
if (!this.isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
this.$router.push('/');
|
||||
},
|
||||
getDefaultLinks() {
|
||||
@@ -333,9 +320,6 @@ export default {
|
||||
mutations.closeHovers();
|
||||
},
|
||||
openSidebarLinksPrompt() {
|
||||
if (!this.isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
mutations.showHover({
|
||||
name: "SidebarLinks",
|
||||
});
|
||||
@@ -423,12 +407,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.no-edit-options {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.share-info-section {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 1em;
|
||||
padding-bottom: 0.5em;
|
||||
border-bottom: 1px solid var(--borderColor);
|
||||
@@ -643,7 +627,6 @@ a.sidebar-link-button {
|
||||
.realtime-pulse.warning>.center {
|
||||
fill: rgb(255, 157, 0);
|
||||
}
|
||||
|
||||
.vue-simple-progress {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
@@ -657,25 +640,4 @@ a.sidebar-link-button {
|
||||
.edit-share-button .link-icon {
|
||||
color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.sidebar-divider-container {
|
||||
width: 100%;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.sidebar-divider {
|
||||
width: 50%;
|
||||
border: none;
|
||||
border-top: 0.1em solid var(--divider);
|
||||
}
|
||||
|
||||
.sidebar-divider-text {
|
||||
display: flex;
|
||||
font-size: .85em;
|
||||
color: var(--textSecondary);
|
||||
letter-spacing: .05em;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,6 @@
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
|
||||
transition: .1s ease all;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button--block {
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "الانتقال إلى العنصر",
|
||||
"watchFile": "ملف المشاهدة",
|
||||
"showMore": "إظهار الخيارات المتقدمة",
|
||||
"showLess": "إخفاء الخيارات المتقدمة",
|
||||
"goToSource": "الانتقال إلى موقع المصدر"
|
||||
"showLess": "إخفاء الخيارات المتقدمة"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Create an account",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "خيارات المستخدم المسؤول",
|
||||
"searchOptions": "خيارات البحث",
|
||||
"systemAdmin": "النظام والإدارة",
|
||||
"configViewer": "عارض التكوين",
|
||||
"configViewerShowFull": "إظهار التكوين الكامل",
|
||||
"configViewerShowComments": "إظهار التعليقات",
|
||||
"configViewerLoadConfig": "تكوين التحميل",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "الوكيل"
|
||||
},
|
||||
"newUserHintSubFolder": "على سبيل المثال '/مجلد فرعي' - فارغ للافتراضي",
|
||||
"permissionsHelp": "تتحكم أذونات المستخدم في الوصول إلى نوع الإجراءات التي يمكن للمستخدم تنفيذها. يمنح الوصول إلى المسؤول التحكم الكامل لتحديث وتغيير إعدادات أي مستخدم بالإضافة إلى العديد من إعدادات النظام.",
|
||||
"configViewer": "عارض التكوين"
|
||||
"permissionsHelp": "تتحكم أذونات المستخدم في الوصول إلى نوع الإجراءات التي يمكن للمستخدم تنفيذها. يمنح الوصول إلى المسؤول التحكم الكامل لتحديث وتغيير إعدادات أي مستخدم بالإضافة إلى العديد من إعدادات النظام."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "إعدادات الموقع",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "بحث وتحديد أيقونة من مكتبة الأيقونات المادية",
|
||||
"shareLinksUpdatedSuccess": "تم تحديث روابط الشريط الجانبي المشاركة بنجاح!",
|
||||
"shareHome": "مشاركة الصفحة الرئيسية",
|
||||
"linkIconPlaceholder": "تحديد اسم رمز المادة اختياريًا",
|
||||
"dividerNamePlaceholder": "نص المقسم (اتركه فارغًا لإظهار سطر)"
|
||||
"linkIconPlaceholder": "تحديد اسم رمز المادة اختياريًا"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "تم نسخ الملف"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "الخطوط{suffix}",
|
||||
"lastUpdate": "آخر تحديث{suffix}",
|
||||
"latency": "الكمون{suffix}",
|
||||
"all": "الكل{suffix}",
|
||||
"expiration": "انتهاء الصلاحية{suffix}",
|
||||
"divider": "مقسم{suffix}"
|
||||
"all": "الكل{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "قواعد الوصول",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Přejít na položku",
|
||||
"watchFile": "Sledovat soubor",
|
||||
"showMore": "Zobrazit rozšířené možnosti",
|
||||
"showLess": "Skrytí pokročilých možností",
|
||||
"goToSource": "Přejít na zdrojové umístění"
|
||||
"showLess": "Skrytí pokročilých možností"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stáhnout soubor",
|
||||
@@ -118,8 +117,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Vytvořit účet",
|
||||
@@ -459,6 +457,7 @@
|
||||
"userUpdated": "Uživatel aktualizován!",
|
||||
"adminOptions": "Možnosti administrátora",
|
||||
"systemAdmin": "Systém a správce",
|
||||
"configViewer": "Prohlížeč konfigurace",
|
||||
"configViewerShowFull": "Zobrazit úplnou konfiguraci",
|
||||
"configViewerShowComments": "Zobrazit komentáře",
|
||||
"configViewerLoadConfig": "Konfigurace načítání",
|
||||
@@ -470,8 +469,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "např. '/subfolder' - prázdný pro výchozí nastavení",
|
||||
"permissionsHelp": "Oprávnění uživatele řídí přístup k typům akcí, které může uživatel provádět. Přístup administrátora poskytuje plnou kontrolu nad aktualizací a změnou nastavení libovolného uživatele i nad mnoha nastaveními systému.",
|
||||
"configViewer": "Prohlížeč konfigurace"
|
||||
"permissionsHelp": "Oprávnění uživatele řídí přístup k typům akcí, které může uživatel provádět. Přístup administrátora poskytuje plnou kontrolu nad aktualizací a změnou nastavení libovolného uživatele i nad mnoha nastaveními systému."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Nastavení webu",
|
||||
@@ -509,8 +507,7 @@
|
||||
"pickIconDescription": "Vyhledejte a vyberte ikonu z knihovny Material Icons.",
|
||||
"shareLinksUpdatedSuccess": "Odkazy na sdílení postranního panelu byly úspěšně aktualizovány!",
|
||||
"shareHome": "Sdílet Domů",
|
||||
"linkIconPlaceholder": "Volitelně zadejte název ikony materiálu",
|
||||
"dividerNamePlaceholder": "Text oddělovače (pro zobrazení řádku nechte prázdný)"
|
||||
"linkIconPlaceholder": "Volitelně zadejte název ikony materiálu"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Odkaz zkopírován!"
|
||||
@@ -708,9 +705,7 @@
|
||||
"lines": "Řádky{suffix}",
|
||||
"lastUpdate": "Poslední aktualizace{suffix}",
|
||||
"latency": "Zpoždění{suffix}",
|
||||
"all": "Všechny{suffix}",
|
||||
"expiration": "Expirace{suffix}",
|
||||
"divider": "Rozdělovač{suffix}"
|
||||
"all": "Všechny{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Pravidla přístupu",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Weiter zu Artikel",
|
||||
"watchFile": "Datei beobachten",
|
||||
"showMore": "Erweiterte Optionen anzeigen",
|
||||
"showLess": "Erweiterte Optionen ausblenden",
|
||||
"goToSource": "Zum Quellort gehen"
|
||||
"showLess": "Erweiterte Optionen ausblenden"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Datei herunterladen",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Konto erstellen",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Optionen für Verwaltungsbenutzer",
|
||||
"searchOptions": "Suchoptionen",
|
||||
"systemAdmin": "System & Verwaltung",
|
||||
"configViewer": "Config Viewer",
|
||||
"configViewerShowFull": "Vollständige Konfiguration anzeigen",
|
||||
"configViewerShowComments": "Kommentare anzeigen",
|
||||
"configViewerLoadConfig": "Konfiguration laden",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "z.B. '/subfolder' - leer für Standard",
|
||||
"permissionsHelp": "Die Benutzerrechte steuern den Zugriff auf die Art der Aktionen, die der Benutzer durchführen kann. Der Administrator-Zugang gibt Ihnen die volle Kontrolle über die Aktualisierung und Änderung der Benutzereinstellungen sowie vieler Systemeinstellungen.",
|
||||
"configViewer": "Config Viewer"
|
||||
"permissionsHelp": "Die Benutzerrechte steuern den Zugriff auf die Art der Aktionen, die der Benutzer durchführen kann. Der Administrator-Zugang gibt Ihnen die volle Kontrolle über die Aktualisierung und Änderung der Benutzereinstellungen sowie vieler Systemeinstellungen."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Seiteneinstellungen",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Suchen und wählen Sie ein Symbol aus der Bibliothek der Materialsymbole",
|
||||
"shareLinksUpdatedSuccess": "Links in der Seitenleiste erfolgreich aktualisiert!",
|
||||
"shareHome": "Startseite teilen",
|
||||
"linkIconPlaceholder": "Optional können Sie einen Materialsymbolnamen angeben",
|
||||
"dividerNamePlaceholder": "Teilertext (leer lassen, um eine Zeile anzuzeigen)"
|
||||
"linkIconPlaceholder": "Optional können Sie einen Materialsymbolnamen angeben"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link wurde kopiert!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Zeilen{suffix}",
|
||||
"lastUpdate": "Letzte Aktualisierung{suffix}",
|
||||
"latency": "Latenzzeit{suffix}",
|
||||
"all": "Alle{suffix}",
|
||||
"expiration": "Verfallsdatum{suffix}",
|
||||
"divider": "Teiler{suffix}"
|
||||
"all": "Alle{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Zugangsregeln",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Πηγαίνετε στο στοιχείο",
|
||||
"watchFile": "Παρακολουθήστε το αρχείο",
|
||||
"showMore": "Εμφάνιση προηγμένων επιλογών",
|
||||
"showLess": "Απόκρυψη προηγμένων επιλογών",
|
||||
"goToSource": "Πηγαίνετε στην τοποθεσία της πηγής"
|
||||
"showLess": "Απόκρυψη προηγμένων επιλογών"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Λήψη αρχείου",
|
||||
@@ -118,8 +117,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Δημιουργία λογαριασμού",
|
||||
@@ -325,6 +323,7 @@
|
||||
"adminOptions": "Επιλογές χρήστη διαχειριστή",
|
||||
"searchOptions": "Επιλογές αναζήτησης",
|
||||
"systemAdmin": "Σύστημα & Διαχείριση",
|
||||
"configViewer": "Config Viewer",
|
||||
"configViewerShowFull": "Εμφάνιση πλήρων ρυθμίσεων",
|
||||
"configViewerShowComments": "Εμφάνιση σχολίων",
|
||||
"configViewerLoadConfig": "Φόρτωση Config",
|
||||
@@ -336,8 +335,7 @@
|
||||
"proxy": "Πληρεξούσιος"
|
||||
},
|
||||
"newUserHintSubFolder": "π.χ. '/subfolder' - κενό για προεπιλογή",
|
||||
"permissionsHelp": "Τα δικαιώματα χρήστη ελέγχουν την πρόσβαση σε είδος ενεργειών που μπορεί να εκτελέσει ο χρήστης. Η πρόσβαση διαχειριστή παρέχει πλήρη έλεγχο για την ενημέρωση και την αλλαγή των ρυθμίσεων οποιουδήποτε χρήστη, καθώς και πολλών ρυθμίσεων του συστήματος.",
|
||||
"configViewer": "Config Viewer"
|
||||
"permissionsHelp": "Τα δικαιώματα χρήστη ελέγχουν την πρόσβαση σε είδος ενεργειών που μπορεί να εκτελέσει ο χρήστης. Η πρόσβαση διαχειριστή παρέχει πλήρη έλεγχο για την ενημέρωση και την αλλαγή των ρυθμίσεων οποιουδήποτε χρήστη, καθώς και πολλών ρυθμίσεων του συστήματος."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Ρυθμίσεις ιστότοπου",
|
||||
@@ -375,8 +373,7 @@
|
||||
"pickIconDescription": "Αναζήτηση και επιλογή ενός εικονιδίου από τη βιβλιοθήκη Material Icons",
|
||||
"shareLinksUpdatedSuccess": "Οι σύνδεσμοι της πλαϊνής μπάρας κοινής χρήσης ενημερώθηκαν με επιτυχία!",
|
||||
"shareHome": "Μοιραστείτε το Home",
|
||||
"linkIconPlaceholder": "Προαιρετικά καθορίστε ένα όνομα εικονιδίου υλικού",
|
||||
"dividerNamePlaceholder": "Κείμενο διαχωριστικού (αφήστε το κενό για να εμφανιστεί μια γραμμή)"
|
||||
"linkIconPlaceholder": "Προαιρετικά καθορίστε ένα όνομα εικονιδίου υλικού"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Γραμμές{suffix}",
|
||||
"lastUpdate": "Τελευταία ενημέρωση{suffix}",
|
||||
"latency": "Καθυστέρηση{suffix}",
|
||||
"all": "Όλα{suffix}",
|
||||
"expiration": "Λήξη{suffix}",
|
||||
"divider": "Διαχωριστικό{suffix}"
|
||||
"all": "Όλα{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Κανόνες πρόσβασης",
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
"incompleteResultsDetails": "Showing max results ({max}). Try narrowing your search to get more results under {max}."
|
||||
},
|
||||
"general": {
|
||||
"divider": "Divider{suffix}",
|
||||
"expiration": "Expiration{suffix}",
|
||||
"all": "All{suffix}",
|
||||
"name": "Name{suffix}",
|
||||
"path": "Path{suffix}",
|
||||
@@ -151,8 +149,7 @@
|
||||
"saveAndExit": "Save and Exit",
|
||||
"keepEditing": "Keep Editing",
|
||||
"closeAnyway": "Close Anyways",
|
||||
"goToItem": "Go to Item",
|
||||
"goToSource": "Go to Source Location"
|
||||
"goToItem": "Go to Item"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -238,8 +235,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Create an account",
|
||||
@@ -594,7 +590,6 @@
|
||||
"adminOptions": "Admin user options"
|
||||
},
|
||||
"sidebar": {
|
||||
"dividerNamePlaceholder": "Divider text (leave blank to show a line)",
|
||||
"siteSettings": "Site Settings",
|
||||
"updateIsAvailable": "An update is available!",
|
||||
"fileActions": "File actions",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Ir al elemento",
|
||||
"watchFile": "Ver archivo",
|
||||
"showMore": "Mostrar opciones avanzadas",
|
||||
"showLess": "Ocultar opciones avanzadas",
|
||||
"goToSource": "Ir a la ubicación de origen"
|
||||
"showLess": "Ocultar opciones avanzadas"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descargar archivo",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Crear cuenta",
|
||||
@@ -328,13 +326,13 @@
|
||||
"adminOptions": "Opciones de administrador",
|
||||
"searchOptions": "Opciones de busqueda",
|
||||
"systemAdmin": "Sistema y administración",
|
||||
"configViewer": "Visor de configuración",
|
||||
"configViewerShowFull": "Mostrar configuración completa",
|
||||
"configViewerShowComments": "Mostrar comentarios",
|
||||
"configViewerLoadConfig": "Cargar configuración",
|
||||
"brandingHelp": "Puedes personalizar la apariencia de FileBrowser Quantum cambiando el nombre, el logo, puedes añadir tus propios estilos personalizados, desactivar los enlaces externos a GitHub, etc. \nSi quieres saber más, visita {0}.",
|
||||
"commandRunnerHelp": "Aquí puede establecer los comandos que se ejecutan en los eventos nombrados. Debe escribir uno por línea. Las variables de entorno {0} y {1} estarán disponibles, siendo {0} relativa a {1}. Para más información sobre esta característica y las variables de entorno disponibles, por favor lea el {2}.",
|
||||
"permissionsHelp": "Los permisos de usuario controlan el acceso al tipo de acciones que el usuario puede realizar. El acceso de administrador proporciona un control total para actualizar y cambiar la configuración de cualquier usuario, así como muchos ajustes del sistema.",
|
||||
"configViewer": "Visor de configuración"
|
||||
"permissionsHelp": "Los permisos de usuario controlan el acceso al tipo de acciones que el usuario puede realizar. El acceso de administrador proporciona un control total para actualizar y cambiar la configuración de cualquier usuario, así como muchos ajustes del sistema."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Ajustes del sitio",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Aquí puedes explorar los iconos más populates de la biblioteca \"Material Icons\"",
|
||||
"shareLinksUpdatedSuccess": "¡Se actualizaron los accesos directos del recurso compartido!",
|
||||
"shareHome": "Recurso compartido",
|
||||
"linkIconPlaceholder": "Opcionalmente, puedes elegir un Material Icon que más te guste",
|
||||
"dividerNamePlaceholder": "Texto divisor (déjelo en blanco para mostrar una línea)"
|
||||
"linkIconPlaceholder": "Opcionalmente, puedes elegir un Material Icon que más te guste"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "¡Enlace copiado!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Líneas{suffix}",
|
||||
"lastUpdate": "Última actualización{suffix}",
|
||||
"latency": "Latencia{suffix}",
|
||||
"all": "Todos{suffix}",
|
||||
"expiration": "Caducidad{suffix}",
|
||||
"divider": "Divisor{suffix}"
|
||||
"all": "Todos{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Reglas de acceso",
|
||||
|
||||
+19
-24
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Aller à l'article",
|
||||
"watchFile": "Regarder le dossier",
|
||||
"showMore": "Afficher les options avancées",
|
||||
"showLess": "Masquer les options avancées",
|
||||
"goToSource": "Aller à l'emplacement de la source"
|
||||
"showLess": "Masquer les options avancées"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Télécharger le fichier",
|
||||
@@ -59,9 +58,9 @@
|
||||
"itemSelected": "élément sélectionné",
|
||||
"itemsSelected": "éléments sélectionnés",
|
||||
"myFiles": "Mes dossiers{suffix}",
|
||||
"newFile": "Nouveau fichier{suffix}",
|
||||
"newFile": "Nouveau Fichier{suffix}",
|
||||
"newFolder": "Nouveau dossier{suffix}",
|
||||
"duration": "Durée{suffix}",
|
||||
"duration": "Durée de l'accord{suffix}",
|
||||
"sortByDuration": "Trier par durée",
|
||||
"sameFolder": "Impossible de déplacer vers le même dossier",
|
||||
"cannotAccesDir": "Impossible d'accéder au répertoire",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Créer un compte",
|
||||
@@ -311,7 +309,7 @@
|
||||
"userHomeBasePath": "Chemin de base pour les répertoires personnels des utilisateurs",
|
||||
"userScopeGenerationPlaceholder": "Le champ d'application sera généré automatiquement",
|
||||
"createUserHomeDirectory": "Créer le répertoire personnel de l'utilisateur",
|
||||
"api": "API",
|
||||
"api": "Clés API",
|
||||
"loginMethodDescription": "méthode de connexion autorisée pour l'utilisateur.",
|
||||
"userNotAdmin": "L'utilisateur n'est pas un administrateur",
|
||||
"enterPassword": "Saisir le mot de passe",
|
||||
@@ -322,7 +320,7 @@
|
||||
"adminOptions": "Options de l'utilisateur administrateur",
|
||||
"searchOptions": "Options de recherche",
|
||||
"systemAdmin": "Système et administration",
|
||||
"configViewer": "Visualiseur de configuration",
|
||||
"configViewer": "Config Viewer",
|
||||
"configViewerShowFull": "Afficher la configuration complète",
|
||||
"configViewerShowComments": "Afficher les commentaires",
|
||||
"configViewerLoadConfig": "Configuration de la charge",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Rechercher et sélectionner une icône dans la bibliothèque des icônes matérielles",
|
||||
"shareLinksUpdatedSuccess": "Les liens de la barre latérale de partage ont été mis à jour avec succès !",
|
||||
"shareHome": "Partager l'accueil",
|
||||
"linkIconPlaceholder": "Spécifier éventuellement un nom d'icône de matériau",
|
||||
"dividerNamePlaceholder": "Texte du séparateur (laisser en blanc pour montrer une ligne)"
|
||||
"linkIconPlaceholder": "Spécifier éventuellement un nom d'icône de matériau"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Lien copié !"
|
||||
@@ -479,6 +476,7 @@
|
||||
"description": "Les clés API sont basées sur l'utilisateur qui les a créées. Voir les",
|
||||
"swaggerLinkText": "page swagger",
|
||||
"created": "Créée",
|
||||
"expires": "Expiration",
|
||||
"actions": "Actions",
|
||||
"enabled": "Activée",
|
||||
"disabled": "Désactivée",
|
||||
@@ -486,8 +484,7 @@
|
||||
"tokenType": "Type de jeton :",
|
||||
"clickToCopyKey": "Cliquez pour copier la clé (cachée)",
|
||||
"minimalDescription": "Jeton minimal",
|
||||
"minimalInfo": "Exclut les personnalisations du jeton afin de réduire la longueur du jeton. Les autorisations correspondent exactement à celles de l'utilisateur associé.",
|
||||
"expires": "Expiration"
|
||||
"minimalInfo": "Exclut les personnalisations du jeton afin de réduire la longueur du jeton. Les autorisations correspondent exactement à celles de l'utilisateur associé."
|
||||
},
|
||||
"index": {
|
||||
"assessment": "Évaluation",
|
||||
@@ -497,7 +494,7 @@
|
||||
"settingsHover": "Paramètres pour l'utilisateur",
|
||||
"toggleClick": "Activation du mode simple clic",
|
||||
"toggleDark": "Mode sombre/clair",
|
||||
"toggleSticky": "Activer/Désactiver le mode persistant",
|
||||
"toggleSticky": "Activer/Désactiver le mode persistent",
|
||||
"noSources": "Aucune source n'est disponible pour cet utilisateur. Ajoutez une source pour cet utilisateur ou configurez une source avec defaultEnabled : true",
|
||||
"unknown": "Inconnu",
|
||||
"simple": "Simple",
|
||||
@@ -528,11 +525,11 @@
|
||||
"signup": "S'inscrire",
|
||||
"password": "Mot de passe",
|
||||
"username": "Nom d'utilisateur",
|
||||
"files": "Fichiers",
|
||||
"files": "Dossiers",
|
||||
"folders": "Dossiers",
|
||||
"exit": "Sortie",
|
||||
"permanent": "Permanent",
|
||||
"hash": "Hachage",
|
||||
"hash": "Hachures",
|
||||
"of": "de",
|
||||
"source": "Source{suffix}",
|
||||
"percentage": "{percentage}%",
|
||||
@@ -572,7 +569,7 @@
|
||||
"documentation": "la documentation{suffix}",
|
||||
"examples": "Exemples{suffix}",
|
||||
"dark": "Sombre{suffix}",
|
||||
"light": "Clair{suffix}",
|
||||
"light": "Lumière{suffix}",
|
||||
"title": "Thème{suffix}",
|
||||
"expires": "Expiration{suffix}",
|
||||
"scope": "Champ d'application{suffix}",
|
||||
@@ -607,12 +604,12 @@
|
||||
"select": "Sélectionner{suffix}",
|
||||
"share": "Partager{suffix}",
|
||||
"update": "Mise à jour{suffix}",
|
||||
"upload": "Importer{suffix}",
|
||||
"upload": "Télécharger{suffix}",
|
||||
"openFile": "Ouvrir un fichier{suffix}",
|
||||
"continue": "Continuer{suffix}",
|
||||
"edit": "Éditer{suffix}",
|
||||
"edit": "Editer{suffix}",
|
||||
"pause": "Pause{suffix}",
|
||||
"resume": "Reprendre{suffix}",
|
||||
"resume": "Curriculum vitae{suffix}",
|
||||
"retry": "Réessayer{suffix}",
|
||||
"disable": "Désactiver{suffix}",
|
||||
"confirm": "Confirmer{suffix}",
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Lignes{suffix}",
|
||||
"lastUpdate": "Dernière mise à jour{suffix}",
|
||||
"latency": "Temps de latence{suffix}",
|
||||
"all": "Tous{suffix}",
|
||||
"expiration": "Expiration{suffix}",
|
||||
"divider": "Diviseur{suffix}"
|
||||
"all": "Tous{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Règles d'accès",
|
||||
@@ -775,11 +770,11 @@
|
||||
"video": "Vidéo",
|
||||
"image": "Image",
|
||||
"audio": "Audio",
|
||||
"archive": "Archive",
|
||||
"archive": "Archives",
|
||||
"document": "Document",
|
||||
"binary": "Binaire",
|
||||
"directory": "Annuaire",
|
||||
"other": "Autre"
|
||||
"other": "Autres"
|
||||
},
|
||||
"tools": {
|
||||
"description": "Utilitaires et outils d'analyse pour vos fichiers",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "עבור לפריט",
|
||||
"watchFile": "צפה בקובץ",
|
||||
"showMore": "הצג אפשרויות מתקדמות",
|
||||
"showLess": "הסתר אפשרויות מתקדמות",
|
||||
"goToSource": "עבור למיקום המקור"
|
||||
"showLess": "הסתר אפשרויות מתקדמות"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "הורד קובץ",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "צור חשבון",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "אפשרויות משתמש מנהל",
|
||||
"searchOptions": "אפשרויות חיפוש",
|
||||
"systemAdmin": "מערכת וניהול",
|
||||
"configViewer": "מציג תצורה",
|
||||
"configViewerShowFull": "הצג את התצורה המלאה",
|
||||
"configViewerShowComments": "הצג תגובות",
|
||||
"configViewerLoadConfig": "טען תצורה",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "פרוקסי"
|
||||
},
|
||||
"newUserHintSubFolder": "לדוגמה, '/subfolder' - ריק עבור ברירת המחדל",
|
||||
"permissionsHelp": "הרשאות המשתמש שולטות בגישה לסוגי הפעולות שהמשתמש יכול לבצע. גישה לניהול מעניקה שליטה מלאה לעדכון ולשינוי הגדרות המשתמש, כמו גם הגדרות רבות של המערכת.",
|
||||
"configViewer": "מציג תצורה"
|
||||
"permissionsHelp": "הרשאות המשתמש שולטות בגישה לסוגי הפעולות שהמשתמש יכול לבצע. גישה לניהול מעניקה שליטה מלאה לעדכון ולשינוי הגדרות המשתמש, כמו גם הגדרות רבות של המערכת."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "הגדרות",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "חפש ובחר סמל מספריית Material Icons.",
|
||||
"shareLinksUpdatedSuccess": "קישורי הסרגל הצדדי שותפו בהצלחה!",
|
||||
"shareHome": "שתף בית",
|
||||
"linkIconPlaceholder": "אפשר לציין שם סמל חומר",
|
||||
"dividerNamePlaceholder": "טקסט מפריד (השאר ריק כדי להציג קו)"
|
||||
"linkIconPlaceholder": "אפשר לציין שם סמל חומר"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "הקישור הועתק!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "קווים{suffix}",
|
||||
"lastUpdate": "עדכון אחרון{suffix}",
|
||||
"latency": "חביון{suffix}",
|
||||
"all": "כל{suffix}",
|
||||
"expiration": "תוקף{suffix}",
|
||||
"divider": "מפריד{suffix}"
|
||||
"all": "כל{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "כללי גישה",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Tovább a tételhez",
|
||||
"watchFile": "Nézze meg a fájlt",
|
||||
"showMore": "Speciális beállítások megjelenítése",
|
||||
"showLess": "Speciális beállítások elrejtése",
|
||||
"goToSource": "Menj a forráshelyre"
|
||||
"showLess": "Speciális beállítások elrejtése"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Fájl letöltése",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Fiók létrehozása",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Admin felhasználói beállítások",
|
||||
"searchOptions": "Keresési lehetőségek",
|
||||
"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",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "pl. '/subfolder' - üres az alapértelmezéshez",
|
||||
"permissionsHelp": "A felhasználói engedélyek szabályozzák, hogy a felhasználó milyen típusú műveleteket hajthat végre. A rendszergazdai hozzáférés teljes körű ellenőrzést biztosít bármely felhasználó beállításainak, valamint számos rendszerbeállításnak a frissítéséhez és módosításához.",
|
||||
"configViewer": "Konfiguráció megtekintő"
|
||||
"permissionsHelp": "A felhasználói engedélyek szabályozzák, hogy a felhasználó milyen típusú műveleteket hajthat végre. A rendszergazdai hozzáférés teljes körű ellenőrzést biztosít bármely felhasználó beállításainak, valamint számos rendszerbeállításnak a frissítéséhez és módosításához."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Oldalbeállítások",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Keressen és válasszon ki egy ikont az Anyagi ikonok könyvtárból.",
|
||||
"shareLinksUpdatedSuccess": "Megosztás oldalsáv linkek sikeresen frissítve!",
|
||||
"shareHome": "Megosztás",
|
||||
"linkIconPlaceholder": "Opcionálisan megadhatja az anyag ikon nevét",
|
||||
"dividerNamePlaceholder": "Választószöveg (üresen hagyva egy sor megjelenítéséhez)"
|
||||
"linkIconPlaceholder": "Opcionálisan megadhatja az anyag ikon nevét"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link másolva!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Vonalak{suffix}",
|
||||
"lastUpdate": "Utolsó frissítés{suffix}",
|
||||
"latency": "Késleltetés{suffix}",
|
||||
"all": "Minden{suffix}",
|
||||
"expiration": "Lejárat{suffix}",
|
||||
"divider": "Választó{suffix}"
|
||||
"all": "Minden{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Hozzáférési szabályok",
|
||||
|
||||
+59
-52
@@ -1,21 +1,19 @@
|
||||
// i18n.js
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
// Import translations (alphabetical order)
|
||||
// Import translations
|
||||
import he from './he.json';
|
||||
import hu from './hu.json';
|
||||
import ar from './ar.json';
|
||||
import cz from './cz.json';
|
||||
import de from './de.json';
|
||||
import el from './el.json';
|
||||
import en from './en.json';
|
||||
import es from './es.json';
|
||||
import fr from './fr.json';
|
||||
import he from './he.json';
|
||||
import hu from './hu.json';
|
||||
import is from './is.json';
|
||||
import it from './it.json';
|
||||
import ja from './ja.json';
|
||||
import ko from './ko.json';
|
||||
import nl from './nl.json';
|
||||
import nlBE from './nl-be.json';
|
||||
import pl from './pl.json';
|
||||
import pt from './pt.json';
|
||||
@@ -23,35 +21,14 @@ import ptBR from './pt-br.json';
|
||||
import ro from './ro.json';
|
||||
import ru from './ru.json';
|
||||
import sk from './sk.json';
|
||||
import svSE from './sv-se.json';
|
||||
import ua from './ua.json';
|
||||
import svSE from './sv-se.json';
|
||||
import zhCN from './zh-cn.json';
|
||||
import zhTW from './zh-tw.json';
|
||||
import cz from './cz.json';
|
||||
|
||||
type LocaleMap = { [key: string]: string };
|
||||
|
||||
// Map of all imported translation modules (alphabetical order)
|
||||
// This is the single source for all translations
|
||||
const translationModules: { [key: string]: any } = {
|
||||
ar, cz, de, el, en, es, fr, he, hu, is, it, ja, ko, nl, nlBE, pl, pt, ptBR, ro, ru, sk, svSE, ua, zhCN, zhTW
|
||||
};
|
||||
|
||||
// Shared list of available locales for the application
|
||||
// Auto-generated from translation modules
|
||||
export const availableLocales: LocaleMap = Object.keys(translationModules).reduce((acc, key) => {
|
||||
// Convert internal keys to their display format
|
||||
const displayMap: { [key: string]: string } = {
|
||||
nlBE: 'nl-be',
|
||||
ptBR: 'pt-br',
|
||||
svSE: 'sv-se',
|
||||
zhCN: 'zh-cn',
|
||||
zhTW: 'zh-tw',
|
||||
};
|
||||
acc[key] = displayMap[key] || key;
|
||||
return acc;
|
||||
}, {} as LocaleMap);
|
||||
|
||||
// Maps internal locale names to standard BCP 47 codes (for navigator.language)
|
||||
export const internalToStandardLocaleMap: { [key: string]: string } = {
|
||||
nlBE: 'nl-be',
|
||||
ptBR: 'pt-br',
|
||||
@@ -67,38 +44,43 @@ export function toStandardLocale(locale: string): string {
|
||||
}
|
||||
|
||||
export function detectLocale(): string {
|
||||
const browserLocale = navigator.language.toLowerCase();
|
||||
|
||||
// Map of browser locale codes to internal locale keys
|
||||
const browserToInternalMap: LocaleMap = {
|
||||
const locale = navigator.language.toLowerCase();
|
||||
const localeMap: LocaleMap = {
|
||||
'he': 'he',
|
||||
'hu': 'hu',
|
||||
'ar': 'ar',
|
||||
'el': 'el',
|
||||
'es': 'es',
|
||||
'en': 'en',
|
||||
'is': 'is',
|
||||
'it': 'it',
|
||||
'fr': 'fr',
|
||||
'pt-br': 'ptBR',
|
||||
'pt': 'pt',
|
||||
'ja': 'ja',
|
||||
'zh-tw': 'zhTW',
|
||||
'zh-cn': 'zhCN',
|
||||
'zh': 'zhCN',
|
||||
'de': 'de',
|
||||
'ro': 'ro',
|
||||
'ru': 'ru',
|
||||
'pl': 'pl',
|
||||
'ko': 'ko',
|
||||
'cz': 'cz',
|
||||
'sk': 'sk',
|
||||
'tr': 'tr',
|
||||
'uk': 'uk',
|
||||
'sv-se': 'svSE',
|
||||
'sv': 'svSE',
|
||||
'nl-be': 'nlBE',
|
||||
};
|
||||
|
||||
// Check for exact matches first (including variants like pt-br)
|
||||
if (browserToInternalMap[browserLocale]) {
|
||||
return browserToInternalMap[browserLocale];
|
||||
for (const key in localeMap) {
|
||||
if (locale.startsWith(key)) {
|
||||
return localeMap[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Check for language prefix matches (e.g., 'en-US' -> 'en')
|
||||
const languagePrefix = browserLocale.split('-')[0];
|
||||
|
||||
// If the language prefix exists in our translations, use it
|
||||
if (translationModules[languagePrefix]) {
|
||||
return languagePrefix;
|
||||
}
|
||||
|
||||
// Try browser-specific mappings for the prefix
|
||||
if (browserToInternalMap[languagePrefix]) {
|
||||
return browserToInternalMap[languagePrefix];
|
||||
}
|
||||
|
||||
return 'en'; // Default fallback
|
||||
return 'en-us'; // Default fallback
|
||||
}
|
||||
|
||||
// List of RTL languages
|
||||
@@ -118,13 +100,38 @@ export function setLocale(locale: string) {
|
||||
}
|
||||
|
||||
|
||||
// Create i18n instance with auto-generated messages from imported modules
|
||||
// Create i18n instance
|
||||
const i18n = createI18n({
|
||||
locale: detectLocale(),
|
||||
fallbackLocale: 'en',
|
||||
// expose i18n.global for outside components
|
||||
legacy: true,
|
||||
messages: translationModules,
|
||||
messages: {
|
||||
he,
|
||||
hu,
|
||||
ar,
|
||||
de,
|
||||
el,
|
||||
en,
|
||||
es,
|
||||
fr,
|
||||
is,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
nlBE,
|
||||
pl,
|
||||
ptBR,
|
||||
pt,
|
||||
ru,
|
||||
ro,
|
||||
sk,
|
||||
svSE,
|
||||
ua,
|
||||
zhCN,
|
||||
zhTW,
|
||||
cz,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Vai all'articolo",
|
||||
"watchFile": "Guarda il file",
|
||||
"showMore": "Mostra opzioni avanzate",
|
||||
"showLess": "Nascondere le opzioni avanzate",
|
||||
"goToSource": "Vai alla posizione della fonte"
|
||||
"showLess": "Nascondere le opzioni avanzate"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Scarica file",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Crea un account",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Opzioni dell'utente amministratore",
|
||||
"searchOptions": "Opzioni di ricerca",
|
||||
"systemAdmin": "Sistema e amministrazione",
|
||||
"configViewer": "Visualizzatore di configurazione",
|
||||
"configViewerShowFull": "Mostra la configurazione completa",
|
||||
"configViewerShowComments": "Mostra i commenti",
|
||||
"configViewerLoadConfig": "Configurazione del carico",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "es. '/sottocartella' - vuoto per l'impostazione predefinita",
|
||||
"permissionsHelp": "I permessi utente controllano l'accesso al tipo di azioni che l'utente può eseguire. L'accesso dell'amministratore consente di aggiornare e modificare le impostazioni di qualsiasi utente e molte impostazioni del sistema.",
|
||||
"configViewer": "Visualizzatore di configurazione"
|
||||
"permissionsHelp": "I permessi utente controllano l'accesso al tipo di azioni che l'utente può eseguire. L'accesso dell'amministratore consente di aggiornare e modificare le impostazioni di qualsiasi utente e molte impostazioni del sistema."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Impostazioni del sito",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Cercare e selezionare un'icona dalla libreria Material Icons.",
|
||||
"shareLinksUpdatedSuccess": "I link della barra laterale di condivisione sono stati aggiornati con successo!",
|
||||
"shareHome": "Condividi Casa",
|
||||
"linkIconPlaceholder": "Specificare facoltativamente il nome dell'icona del materiale",
|
||||
"dividerNamePlaceholder": "Testo del divisore (lasciare vuoto per mostrare una riga)"
|
||||
"linkIconPlaceholder": "Specificare facoltativamente il nome dell'icona del materiale"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link copiato!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Linee{suffix}",
|
||||
"lastUpdate": "Ultimo aggiornamento{suffix}",
|
||||
"latency": "Latenza{suffix}",
|
||||
"all": "Tutti{suffix}",
|
||||
"expiration": "Scadenza{suffix}",
|
||||
"divider": "Divisore{suffix}"
|
||||
"all": "Tutti{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regole di accesso",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "アイテムへ",
|
||||
"watchFile": "ウォッチファイル",
|
||||
"showMore": "詳細オプションを表示",
|
||||
"showLess": "詳細オプションを隠す",
|
||||
"goToSource": "ソース・ロケーションへ"
|
||||
"showLess": "詳細オプションを隠す"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "ファイルをダウンロード",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "アカウントを作成",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "管理者ユーザーオプション",
|
||||
"searchOptions": "検索オプション",
|
||||
"systemAdmin": "システム&管理",
|
||||
"configViewer": "設定ビューア",
|
||||
"configViewerShowFull": "全設定を表示",
|
||||
"configViewerShowComments": "コメントを表示",
|
||||
"configViewerLoadConfig": "設定を読み込み",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "プロキシ"
|
||||
},
|
||||
"newUserHintSubFolder": "例 '/subfolder' - デフォルトは空白",
|
||||
"permissionsHelp": "ユーザー権限は、ユーザーが実行できるアクションの種類へのアクセスを制御します。管理者アクセスは、多くのシステム設定だけでなく、任意のユーザーの設定を更新および変更するための完全なコントロールを提供します。",
|
||||
"configViewer": "コンフィグビューア"
|
||||
"permissionsHelp": "ユーザー権限は、ユーザーが実行できるアクションの種類へのアクセスを制御します。管理者アクセスは、多くのシステム設定だけでなく、任意のユーザーの設定を更新および変更するための完全なコントロールを提供します。"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "サイト設定",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "マテリアル・アイコン・ライブラリからアイコンを検索して選択します。",
|
||||
"shareLinksUpdatedSuccess": "サイドバーの共有リンクが正常に更新されました!",
|
||||
"shareHome": "シェア・ホーム",
|
||||
"linkIconPlaceholder": "オプションで素材アイコン名を指定",
|
||||
"dividerNamePlaceholder": "分割テキスト(行を表示するには空白にする)"
|
||||
"linkIconPlaceholder": "オプションで素材アイコン名を指定"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "リンクがコピーされました!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "ライン{suffix}",
|
||||
"lastUpdate": "最終更新{suffix}",
|
||||
"latency": "レイテンシー{suffix}",
|
||||
"all": "すべて{suffix}",
|
||||
"expiration": "有効期限{suffix}",
|
||||
"divider": "ディバイダー{suffix}"
|
||||
"all": "すべて{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "アクセスルール",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "항목으로 이동",
|
||||
"watchFile": "파일 보기",
|
||||
"showMore": "고급 옵션 표시",
|
||||
"showLess": "고급 옵션 숨기기",
|
||||
"goToSource": "소스 위치로 이동"
|
||||
"showLess": "고급 옵션 숨기기"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "파일 다운로드",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "계정 생성",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "관리자 사용자 옵션",
|
||||
"searchOptions": "검색 옵션",
|
||||
"systemAdmin": "시스템 및 관리자",
|
||||
"configViewer": "구성 뷰어",
|
||||
"configViewerShowFull": "전체 구성 표시",
|
||||
"configViewerShowComments": "댓글 표시",
|
||||
"configViewerLoadConfig": "로드 구성",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "프록시"
|
||||
},
|
||||
"newUserHintSubFolder": "예: '/subfolder' - 기본값은 비어 있습니다.",
|
||||
"permissionsHelp": "사용자 권한은 사용자가 수행할 수 있는 작업 유형에 대한 액세스를 제어합니다. 관리자 권한은 모든 사용자 설정은 물론 여러 시스템 설정을 업데이트하고 변경할 수 있는 모든 권한을 부여합니다.",
|
||||
"configViewer": "구성 뷰어"
|
||||
"permissionsHelp": "사용자 권한은 사용자가 수행할 수 있는 작업 유형에 대한 액세스를 제어합니다. 관리자 권한은 모든 사용자 설정은 물론 여러 시스템 설정을 업데이트하고 변경할 수 있는 모든 권한을 부여합니다."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "사이트 설정",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "머티리얼 아이콘 라이브러리에서 아이콘을 검색하고 선택합니다.",
|
||||
"shareLinksUpdatedSuccess": "공유 사이드바 링크가 성공적으로 업데이트되었습니다!",
|
||||
"shareHome": "홈 공유",
|
||||
"linkIconPlaceholder": "선택적으로 머티리얼 아이콘 이름을 지정합니다.",
|
||||
"dividerNamePlaceholder": "디바이더 텍스트(줄을 표시하려면 비워둡니다)"
|
||||
"linkIconPlaceholder": "선택적으로 머티리얼 아이콘 이름을 지정합니다."
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "링크가 복사되었습니다!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "라인{suffix}",
|
||||
"lastUpdate": "최근 업데이트{suffix}",
|
||||
"latency": "지연 시간{suffix}",
|
||||
"all": "모두{suffix}",
|
||||
"expiration": "만료{suffix}",
|
||||
"divider": "디바이더{suffix}"
|
||||
"all": "모두{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "액세스 규칙",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Ga naar item",
|
||||
"watchFile": "Bestand bekijken",
|
||||
"showMore": "Geavanceerde opties weergeven",
|
||||
"showLess": "Geavanceerde opties verbergen",
|
||||
"goToSource": "Ga naar de bronlocatie"
|
||||
"showLess": "Geavanceerde opties verbergen"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Bestand downloaden",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Account aanmaken",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Opties voor beheerders",
|
||||
"searchOptions": "Zoekopties",
|
||||
"systemAdmin": "Systeem & Beheer",
|
||||
"configViewer": "Configuratiescherm",
|
||||
"configViewerShowFull": "Volledige configuratie weergeven",
|
||||
"configViewerShowComments": "Opmerkingen tonen",
|
||||
"configViewerLoadConfig": "Configuratie laden",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "bijv. '/submap' - leeg voor standaard",
|
||||
"permissionsHelp": "Gebruikersrechten bepalen welke acties de gebruiker kan uitvoeren. Administrar toegang geeft volledige controle om de instellingen van elke gebruiker bij te werken en te wijzigen, evenals vele systeeminstellingen.",
|
||||
"configViewer": "Configuratiescherm"
|
||||
"permissionsHelp": "Gebruikersrechten bepalen welke acties de gebruiker kan uitvoeren. Administrar toegang geeft volledige controle om de instellingen van elke gebruiker bij te werken en te wijzigen, evenals vele systeeminstellingen."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Site-instellingen",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Zoek en selecteer een pictogram uit de bibliotheek met materiaalpictogrammen",
|
||||
"shareLinksUpdatedSuccess": "Zijbalk links delen succesvol bijgewerkt!",
|
||||
"shareHome": "Thuis delen",
|
||||
"linkIconPlaceholder": "Geef optioneel een materiaalpictogramnaam op",
|
||||
"dividerNamePlaceholder": "Scheidingstekst (leeg laten om een lijn weer te geven)"
|
||||
"linkIconPlaceholder": "Geef optioneel een materiaalpictogramnaam op"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link gekopieerd!"
|
||||
@@ -708,9 +705,7 @@
|
||||
"lines": "Lijnen{suffix}",
|
||||
"lastUpdate": "Laatste update{suffix}",
|
||||
"latency": "Latency{suffix}",
|
||||
"all": "Alle{suffix}",
|
||||
"expiration": "Vervaldatum{suffix}",
|
||||
"divider": "Verdeler{suffix}"
|
||||
"all": "Alle{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Toegangsregels",
|
||||
|
||||
@@ -1,856 +0,0 @@
|
||||
{
|
||||
"messages": {
|
||||
"pathNotFound": "Pad bestaat niet op bestandssysteem",
|
||||
"pathNotFoundMessage": "Het pad hiervoor bestaat niet meer op het bestandssysteem. Het kan verplaatst of verwijderd zijn.",
|
||||
"reassignPath": "Pad opnieuw toewijzen",
|
||||
"pathReassigned": "Pad succesvol bijgewerkt!",
|
||||
"pathReassignFailed": "Het bijwerken van het pad is mislukt. Probeer het opnieuw.",
|
||||
"incompleteResultsDetails": "Max resultaten ({max}). Probeer de zoekopdracht aan te passen voor meer resultaten onder {max}."
|
||||
},
|
||||
"general": {
|
||||
"all": "Alle{suffix}",
|
||||
"name": "Naam{suffix}",
|
||||
"path": "Pad{suffix}",
|
||||
"user": "Gebruiker{suffix}",
|
||||
"group": "Groep{suffix}",
|
||||
"signup": "Aanmelden{suffix}",
|
||||
"password": "Wachtwoord{suffix}",
|
||||
"username": "Gebruikersnaam{suffix}",
|
||||
"files": "Bestanden{suffix}",
|
||||
"folders": "Mappen{suffix}",
|
||||
"exit": "Sluiten{suffix}",
|
||||
"permanent": "Permanent{suffix}",
|
||||
"hash": "Hash{suffix}",
|
||||
"of": "van{suffix}",
|
||||
"source": "Bron{suffix}",
|
||||
"percentage": "{percentage}%",
|
||||
"type": "Type{suffix}",
|
||||
"size": "Grootte{suffix}",
|
||||
"body": "Inhoud{suffix}",
|
||||
"home": "Thuis{suffix}",
|
||||
"clear": "Wissen{suffix}",
|
||||
"help": "Hulp{suffix}",
|
||||
"login": "Inloggen{suffix}",
|
||||
"logout": "Afmelden{suffix}",
|
||||
"preview": "Voorbeeld{suffix}",
|
||||
"settings": "Instellingen{suffix}",
|
||||
"sources": "Bronnen{suffix}",
|
||||
"links": "Links{suffix}",
|
||||
"tool": "Gereedschap{suffix}",
|
||||
"used": "gebruikt{suffix}",
|
||||
"status": "Status{suffix}",
|
||||
"analyze": "Analyseren{suffix}",
|
||||
"results": "Resultaten{suffix}",
|
||||
"users": "Gebruikers{suffix}",
|
||||
"scopes": "Bereiken{suffix}",
|
||||
"commands": "Opdrachten{suffix}",
|
||||
"photos": "Foto's",
|
||||
"audio": "Audio{suffix}",
|
||||
"videos": "Video's{suffix}",
|
||||
"documents": "Documenten{suffix}",
|
||||
"archives": "Archieven{suffix}",
|
||||
"images": "Afbeeldingen{suffix}",
|
||||
"music": "Muziek{suffix}",
|
||||
"pdf": "PDF{suffix}",
|
||||
"search": "Zoeken{suffix}",
|
||||
"number": "Aantal{suffix}",
|
||||
"types": "Typen{suffix}",
|
||||
"video": "Video{suffix}",
|
||||
"admin": "Admin{suffix}",
|
||||
"branding": "Branding{suffix}",
|
||||
"language": "Taal{suffix}",
|
||||
"documentation": "documentatie{suffix}",
|
||||
"examples": "Voorbeelden{suffix}",
|
||||
"dark": "Donker{suffix}",
|
||||
"light": "Licht{suffix}",
|
||||
"title": "Thema{suffix}",
|
||||
"expires": "Verloopt op{suffix}",
|
||||
"scope": "Bereik{suffix}",
|
||||
"rules": "Regels{suffix}",
|
||||
"downloads": "Downloads{suffix}",
|
||||
"submit": "Indienen{suffix}",
|
||||
"navigation": "Navigatie{suffix}",
|
||||
"metadata": "Metagegevens{suffix}",
|
||||
"loading": "Laden{suffix}",
|
||||
"add": "Toevoegen{suffix}",
|
||||
"stay": "Blijven{suffix}",
|
||||
"cancel": "Annuleren{suffix}",
|
||||
"change": "Wijzigen{suffix}",
|
||||
"verify": "Controleren{suffix}",
|
||||
"close": "Sluiten{suffix}",
|
||||
"copy": "Kopiëren{suffix}",
|
||||
"create": "Aanmaken{suffix}",
|
||||
"delete": "Verwijderen{suffix}",
|
||||
"download": "Downloaden{suffix}",
|
||||
"file": "Bestand{suffix}",
|
||||
"folder": "Map{suffix}",
|
||||
"info": "Info{suffix}",
|
||||
"more": "Meer{suffix}",
|
||||
"move": "Verplaatsen{suffix}",
|
||||
"new": "Nieuw{suffix}",
|
||||
"next": "Volgende{suffix}",
|
||||
"ok": "OK{suffix}",
|
||||
"previous": "Vorige{suffix}",
|
||||
"publish": "Publiceren{suffix}",
|
||||
"rename": "Hernoemen{suffix}",
|
||||
"replace": "Vervangen{suffix}",
|
||||
"save": "Opslaan{suffix}",
|
||||
"schedule": "Schema{suffix}",
|
||||
"select": "Selecteren{suffix}",
|
||||
"share": "Delen{suffix}",
|
||||
"update": "Bijwerken{suffix}",
|
||||
"upload": "Uploaden{suffix}",
|
||||
"openFile": "Bestand openen{suffix}",
|
||||
"continue": "Doorgaan{suffix}",
|
||||
"edit": "Bewerken{suffix}",
|
||||
"pause": "Pauzeren{suffix}",
|
||||
"resume": "Hervatten{suffix}",
|
||||
"retry": "Opnieuw proberen{suffix}",
|
||||
"disable": "Uitschakelen{suffix}",
|
||||
"confirm": "Bevestigen{suffix}",
|
||||
"uploading": "Uploaden{suffix}",
|
||||
"paused": "Gepauzeerd{suffix}",
|
||||
"conflict": "Conflict{suffix}",
|
||||
"output": "Uitvoer{suffix}",
|
||||
"watching": "Bekijken{suffix}",
|
||||
"lines": "Regels{suffix}",
|
||||
"lastUpdate": "Laatste update{suffix}",
|
||||
"latency": "Vertraging{suffix}",
|
||||
"expiration": "Vervaldatum{suffix}",
|
||||
"divider": "Verdeler{suffix}"
|
||||
},
|
||||
"buttons": {
|
||||
"generateNewOtp": "Herstellen en nieuwe twee-factorcode genereren",
|
||||
"copyFile": "Bestand kopiëren",
|
||||
"copyToClipboard": "Kopiëren naar klembord",
|
||||
"copyDownloadLinkToClipboard": "Downloadlink kopiëren naar klembord",
|
||||
"moveFile": "Bestand verplaatsen",
|
||||
"permalink": "Permanente link aanmaken",
|
||||
"reportIssue": "Probleem melden",
|
||||
"selectMultiple": "Meerdervoudige selectie",
|
||||
"shell": "Opdrachtvenster schakelen",
|
||||
"switchView": "Weergave wisselen",
|
||||
"toggleSidebar": "Zijbalk schakelen",
|
||||
"pauseAll": "Alles onderbreken",
|
||||
"resumeAll": "Alles hervatten",
|
||||
"clearCompleted": "Wissen voltooid",
|
||||
"showMore": "Geavanceerde opties weergeven",
|
||||
"showLess": "Geavanceerde opties verbergen",
|
||||
"openParentFolder": "Bovenliggende map openen",
|
||||
"watchFile": "Bestand bekijken",
|
||||
"listView": "Lijstweergave",
|
||||
"compactView": "Compacte weergave",
|
||||
"normalView": "Normale weergave",
|
||||
"galleryView": "Galerie",
|
||||
"selectedCount": "Aantal items geselecteerd om een actie uit te voeren",
|
||||
"selectAll": "Alles selecteren",
|
||||
"uploadFiles": "Bestanden uploaden",
|
||||
"uploadFolder": "Map uploaden",
|
||||
"openUploadViewer": "Uploadmanager openen",
|
||||
"discardAndExit": "Weggooien en afsluiten",
|
||||
"saveAndExit": "Opslaan en afsluiten",
|
||||
"keepEditing": "Blijven bewerken",
|
||||
"closeAnyway": "Sluiten",
|
||||
"goToItem": "Ga naar item",
|
||||
"goToSource": "Ga naar de bronlocatie"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Bestand downloaden",
|
||||
"downloadFolder": "Map downloaden",
|
||||
"downloadSelected": "Selectie downloaden"
|
||||
},
|
||||
"upload": {
|
||||
"abortUpload": "Weet u zeker dat u wilt afbreken?"
|
||||
},
|
||||
"errors": {
|
||||
"forbidden": "U hebt hier geen toegangsrechten.",
|
||||
"internal": "Er ging iets niet goed.",
|
||||
"notFound": "Geen toegang tot deze locatie.",
|
||||
"connection": "Geen toegang tot de server.",
|
||||
"shareNotFound": "Ongeldige of verlopen gedeelde map.",
|
||||
"failedToConnectToServer": "Kan geen verbinding maken met de server, draait deze nog?"
|
||||
},
|
||||
"files": {
|
||||
"closePreview": "Voorbeeld sluiten",
|
||||
"lastModified": "Laatst bewerkt{suffix}",
|
||||
"duration": "Duur{suffix}",
|
||||
"lonely": "Hier is niets te zien...",
|
||||
"multipleSelectionEnabled": "Meervoudige selectie ingeschakeld",
|
||||
"sortByLastModified": "Sorteren op laatst bewerkt",
|
||||
"sortByName": "Sorteren op naam",
|
||||
"sortBySize": "Sorteren op grootte",
|
||||
"sortByDuration": "Sorteren op duur",
|
||||
"noPreview": "Voorbeeld is niet beschikbaar voor dit bestand.",
|
||||
"noDownloadAccess": "Download is niet beschikbaar.",
|
||||
"itemSelected": "item geselecteerd",
|
||||
"itemsSelected": "items geselecteerd",
|
||||
"myFiles": "Mijn bestanden{suffix}",
|
||||
"newFile": "Nieuw bestand{suffix}",
|
||||
"newFolder": "Nieuwe map{suffix}",
|
||||
"sameFolder": "Kan niet verplaatsen naar dezelfde map",
|
||||
"cannotAccesDir": "Geen toegang tot map"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Functietoetsen",
|
||||
"mouseActions": "Muisacties",
|
||||
"fileOperations": "Bestandsbewerkingen",
|
||||
"click": "selecteer bestand of map",
|
||||
"marqueeSelect": "selecteer meerdere bestanden of mappen door over lege ruimte te slepen",
|
||||
"ctrl": {
|
||||
"click": "meerdere bestanden of mappen selecteren",
|
||||
"c": "geselecteerde items kopiëren",
|
||||
"v": "gekopieerde/geknipte items plakken",
|
||||
"x": "geselecteerde items knippen",
|
||||
"d": "geselecteerde items downloaden",
|
||||
"b": "zijbalk schakelen",
|
||||
"comma": "ga naar instellingen"
|
||||
},
|
||||
"del": "geselecteerde items verwijderen",
|
||||
"esc": "selectie wissen en/of opdrachtvenster sluiten",
|
||||
"f2": "bestand herbenoemen",
|
||||
"f1": "hulp-venster tonen",
|
||||
"slash": "Zoeken inschakelen",
|
||||
"description": "U kunt de basisnavigatieopties hieronder bekijken. Ga voor aanvullende informatie naar",
|
||||
"wiki": "FileBrowser Quantum Officiële documenten"
|
||||
},
|
||||
"languages": {
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
"ar": "العربية",
|
||||
"de": "Deutsch",
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"fr": "Français",
|
||||
"is": "Icelandic",
|
||||
"it": "Italiano",
|
||||
"ja": "日本語",
|
||||
"ko": "한국어",
|
||||
"nlBE": "Dutch (Belgium)",
|
||||
"pl": "Polski",
|
||||
"pt": "Português",
|
||||
"ptBR": "Português (Brasil)",
|
||||
"ro": "Romanian",
|
||||
"ru": "Русский",
|
||||
"sk": "Slovenčina",
|
||||
"svSE": "Swedish (Sweden)",
|
||||
"tr": "Türkçe",
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Account aanmaken",
|
||||
"loginInstead": "Ik heb al een account",
|
||||
"passwordConfirm": "Wachtwoord bevestigen",
|
||||
"passwordsDontMatch": "Wachtwoorden komen niet overeen",
|
||||
"usernameTaken": "Gebruikersnaam al in gebruik",
|
||||
"wrongCredentials": "Verkeerde inloggegevens",
|
||||
"or": "OF",
|
||||
"inProgress": "Inloggen...",
|
||||
"failedLogin": "Inloggen is mislukt",
|
||||
"invalidCredentials": "Ongeldige inloggegevens verstrekt"
|
||||
},
|
||||
"share": {
|
||||
"notice": "Opmerking: wanneer een gedeelde map wordt aangemaakt, wordt deze meer blootgesteld aan het publiek en gelden er andere toegangsregels. Als uw gedeelde map niet veilig is geconfigureerd, kan iedereen er toegang tot krijgen.",
|
||||
"disableAnonymous": "Anonieme toegang uitschakelen",
|
||||
"disableThumbnails": "Miniaturen uitschakelen",
|
||||
"enableAllowedUsernames": "Alleen delen met bepaalde gebruikers",
|
||||
"allowedUsernamesPlaceholder": "Voer gebruikersnamen in, door komma's gescheiden",
|
||||
"disableAnonymousDescription": "Alleen geauthenticeerde gebruikers krijgen toegang tot de gedeelde map. De gebruiker moet toegang hebben tot de bron van de gedeelde map.",
|
||||
"disableThumbnailsDescription": "Er worden geen voorbeeldminiaturen weergegeven in de gedeelde map.",
|
||||
"enableAllowedUsernamesDescription": "Alleen de opgegeven gebruikers krijgen toegang tot de gedeelde map. De gebruiker moet toegang hebben tot de bron van de gedeelde map.",
|
||||
"downloadsLimitDescription": "Het maximum aantal keren dat een bestand/map van de gedeelde map kan worden gedownload. Leeg laten voor onbeperkt.",
|
||||
"maxBandwidthDescription": "De maximale downloadbandbreedte in kilobytes per seconde. Leeg laten voor onbeperkt.",
|
||||
"shareThemeDescription": "Het thema dat moet worden gebruikt voor de link delen.",
|
||||
"keepAfterExpiration": "Gedeelde map niet verwijderen nadat deze is verlopen",
|
||||
"keepAfterExpirationDescription": "De gedeelde map wordt niet verwijderd nadat deze verlopen is. Dit is handig als u de vervaldatum van een gedeelde map wilt verlengen of het verder wilt bewerken nadat het verlopen is.",
|
||||
"saveDisabled": "Kan wijzigingen in gedeelde bestanden niet opslaan.",
|
||||
"shareDurationDescription": "Duur voordat de gedeelde map verloopt. Leeg laten voor een permanente gedeelde map.",
|
||||
"passwordDescription": "Optioneel wachtwoord dat vereist is om toegang te krijgen tot deze gedeelde map, inclusief geauthenticeerde gebruikers.",
|
||||
"allowedUsernamesListDescription": "Door komma's gescheiden gebruikersnamen die toegang hebben tot de gedeelde map (indien ingeschakeld).",
|
||||
"shareThemeColorDescription": "CSS kleurwaarde toegepast op het thema van de gedeelde map (bijv. rood, #0ea5e9, rgb(14,165,233)).",
|
||||
"shareTitleDescription": "Aangepaste paginatitel die wordt weergegeven op de deelpagina.",
|
||||
"shareDescriptionHelp": "Korte beschrijving die wordt weergegeven op de pagina van de gedeelde map (kan worden gebruikt in metatags).",
|
||||
"shareLogoDescription": "URL van logoafbeelding of pad dat toegankelijk is voor de client -- zowel abosolute als index path worden geaccepteerd: bijv. 'https://domain.com/logo.png' of '/path/to/logo.png'.",
|
||||
"shareBannerDescription": "URL van bannerafbeelding of pad dat toegankelijk is voor de client -- zowel abosolute als indexpath worden geaccepteerd: bijv. 'https://domain.com/banner.png' of '/path/to/banner.png'.",
|
||||
"shareFaviconDescription": "Favicon URL of pad toegankelijk door de client -- zowel abosolute als index path worden geaccepteerd: bijv. 'https://domain.com/favicon.png' of '/path/to/favicon.png'.",
|
||||
"hideNavButtons": "Navigatieknoppen verbergen",
|
||||
"hideNavButtonsDescription": "Verberg de navigatieknoppen op de navigatiebalk in de gedeelde map om een minimalistische look te creëren.",
|
||||
"viewModeDescription": "Standaard lay-out voor de gedeelde pagina.",
|
||||
"titleDefault": "Gedeelde bestanden - {title}",
|
||||
"descriptionDefault": "Er is een gedeelde map naar u verzonden om te bekijken of te downloaden.",
|
||||
"descriptionUploadDefault": "U hebt een gedeelde map ontvangen om bestanden te uploaden.",
|
||||
"disableShareCard": "Gedeelde map-kaart uitschakelen",
|
||||
"disableShareCardDescription": "De gedeelde map-kaart uitschakelen op de gedeelde pagina in de zijbalk of bovenaan de pagina op mobiel.",
|
||||
"disableSidebar": "Zijbalk uitschakelen",
|
||||
"disableFileViewer": "Bestandsverkenner uitschakelen",
|
||||
"disableSidebarDescription": "De zijbalk op de gedeelde pagina uitschakelen.",
|
||||
"enforceDarkLightMode": "Themamodus afdwingen",
|
||||
"enforceDarkLightModeDescription": "Een specifieke themamodus (donker of licht) afdwingen voor deze gedeelde map, waarbij gebruikersvoorkeuren worden overschreven.",
|
||||
"defaultViewMode": "Standaard weergavemodus",
|
||||
"defaultViewModeDescription": "Stel de standaard weergavemodus in voor anonieme gebruikers die deze gedeelde map bekijken. Aangemelde gebruikers kunnen nog steeds hun weergavevoorkeur wijzigen.",
|
||||
"default": "Gebruikersvoorkeuren toepassen",
|
||||
"dark": "Donker",
|
||||
"light": "Licht",
|
||||
"enableOnlyOffice": "OnlyOffice Viewer inschakelen",
|
||||
"enableOnlyOfficeDescription": "Het bekijken van Office-bestanden toestaan met OnlyOffice in deze gedeelde map.",
|
||||
"shareType": "Type gedeelde map",
|
||||
"shareTypeDescription": "Het type gedeelde map dat moet worden aangemaakt. Normale gedeelde mappen staan het bekijken en downloaden van bestanden toe. Upload gedeelde mappen staan het uploaden van bestanden naar de gedeelde map toe.",
|
||||
"normalShare": "Normaal",
|
||||
"uploadShare": "Alleen uploaden",
|
||||
"perUserDownloadLimit": "Downloadlimiet op elke gebruiker toepassen",
|
||||
"perUserDownloadLimitDescription": "Indien ingeschakeld, wordt de downloadlimiet toegepast op elke gebruiker. Anonieme gebruikers kunnen geen bestanden downloaden van deze gedeelde map.",
|
||||
"extractEmbeddedSubtitles": "Ingesloten ondertitels uitpakken",
|
||||
"extractEmbeddedSubtitlesDescription": "Ingesloten ondertitels uit mediabestanden halen tijdens het laden -- dit kan traag zijn voor grote bestanden.",
|
||||
"disableLoginOption": "Aanmeldingsoptie verbergen",
|
||||
"disableLoginOptionDescription": "De aanmeldknop in de zijbalk verbergen voor anonieme gebruikers. Als deze optie is ingeschakeld, zien anonieme gebruikers geen optie om in te loggen.",
|
||||
"customizeSidebarLinks": "Koppelingen in de zijbalk aanpassen",
|
||||
"customizeSidebarLinksButton": "Koppelingen aanpassen",
|
||||
"browseFiles": "Bladeren door bestanden",
|
||||
"selectBannerImage": "Banner-afbeelding selecteren",
|
||||
"selectFaviconImage": "Favicon-afbeelding selecteren",
|
||||
"selectImage": "Afbeelding selecteren",
|
||||
"disableDownload": "Het downloaden van bestanden uitschakelen",
|
||||
"disableDownloadDescription": "Niemand kan bestanden downloaden of de inhoud van een bestand bekijken.",
|
||||
"allowModify": "Bewerken van bestanden toestaan",
|
||||
"allowModifyDescription": "Iedereen kan bestanden bewerken in deze gedeelde map.",
|
||||
"allowDelete": "Bestanden verwijderen toestaan",
|
||||
"allowDeleteDescription": "Iedereen kan bestanden en mappen van deze gedeelde map verwijderen.",
|
||||
"allowCreate": "Bestanden en mappen aanmaken en uploaden",
|
||||
"allowCreateDescription": "Iedereen kan nieuwe bestanden en mappen aanmaken en uploaden in deze gedeelde map.",
|
||||
"allowReplacements": "Vervanging van bestanden toestaan",
|
||||
"allowReplacementsDescription": "Iedereen kan bestaande bestanden vervangen door bestanden met dezelfde naam te uploaden.",
|
||||
"shareInfo": "QR-code en info delen",
|
||||
"shareInfoDescription": "Deze koppeling toont de informatie van de gedeelde map, inclusief titel, beschrijving, banner en QR-code in een popup.",
|
||||
"downloadDescription": "Met deze koppeling kunt u de volledige inhoud van de gedeelde map downloaden als een gecomprimeerd bestand."
|
||||
},
|
||||
"prompts": {
|
||||
"operationInProgress": "Werk in uitvoering, even geduld...",
|
||||
"quickJump": "Snel navigeren naar andere bestanden",
|
||||
"downloadsLimit": "Max. aantal downloads",
|
||||
"maxBandwidth": "Max. downloadbandbreedte (kbps)",
|
||||
"shareTheme": "Thema delen",
|
||||
"viewMode": "Weergavemodus",
|
||||
"shareThemeColor": "Themakleur delen (CSS-waarde)",
|
||||
"shareTitle": "Titel delen",
|
||||
"shareDescription": "Beschrijving delen",
|
||||
"shareLogo": "URL van logo delen",
|
||||
"shareBanner": "URL van banner delen",
|
||||
"shareFavicon": "URL van favicon delen",
|
||||
"destinationSource": "Bestemmingsbron:",
|
||||
"selectPath": "Pad kiezen",
|
||||
"copy": "Kopiëren",
|
||||
"copyMessage": "Kies een locatie om uw bestanden te kopiëren: ",
|
||||
"deleteMessageMultiple": "Weet u zeker dat u {count} bestand(en) wilt verwijderen?",
|
||||
"deleteMessageSingle": "Weet u zeker dat u dit bestand/deze map wil verwijderen?",
|
||||
"deleteMessageShare": "Weet u zeker dat u deze gedeelde map wilt verwijderen: {path} ?",
|
||||
"deleteUserMessage": "Weet u zeker dat u deze gebruiker wilt verwijderen?",
|
||||
"deleteTitle": "Bestanden verwijderen",
|
||||
"displayName": "Weergavenaam: ",
|
||||
"download": "Bestanden downloaden",
|
||||
"downloadMessage": "Kies het formaat dat u wilt downloaden. ",
|
||||
"dragAndDrop": "Sleep bestanden hier naartoe",
|
||||
"error": "Er ging iets mis",
|
||||
"completed": "Voltooid",
|
||||
"fileInfo": "Bestandsinformatie",
|
||||
"selected": "geselecteerd",
|
||||
"filesSelected": "bestanden geselecteerd",
|
||||
"lastModified": "Laatst gewijzigd{suffix}",
|
||||
"move": "Verplaatsen",
|
||||
"moveSuccess": "Bestanden succesvol verplaatst!",
|
||||
"newFileSuccess": "Bestand succesvol aangemaakt!",
|
||||
"newDirSuccess": "Map succesvol aangemaakt!",
|
||||
"copySuccess": "Bestanden succesvol gekopieerd!",
|
||||
"moveMessage": "Kies een nieuwe locatie voor de bestand(en)/map(pen)",
|
||||
"newArchetype": "Maak een nieuw bericht op basis van een archetype. Uw bestand wordt aangemaakt in de inhoudsmap.",
|
||||
"newDir": "Nieuwe map",
|
||||
"newDirMessage": "Voer de naam van de nieuwe map in",
|
||||
"newFile": "Nieuw bestand",
|
||||
"newFileMessage": "Voer de naam van het nieuwe bestand in",
|
||||
"numberDirs": "Aantal mappen{suffix}",
|
||||
"numberFiles": "Aantal bestanden{suffix}",
|
||||
"rename": "Hernoemen",
|
||||
"renameSuccess": "Item met succes hernoemd!",
|
||||
"renameMessageConflict": "Een item met de naam \"{filename}\" bestaat al!",
|
||||
"renameMessage": "Voer een nieuwe naam in:",
|
||||
"renameMessageInvalid": "Een bestand kan geen \"/\" of \"\\\" bevatten.",
|
||||
"renameFailed": "Hernoemen van item mislukt",
|
||||
"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",
|
||||
"saveBeforeExit": "Niet-opgeslagen wijzigingen",
|
||||
"saveBeforeExitMessage": "U hebt niet-opgeslagen wijzigingen in de editor. Wat wilt u doen?",
|
||||
"schedule": "Plannen",
|
||||
"scheduleMessage": "Kies een datum en tijd om de publicatie van dit bericht in te plannen.",
|
||||
"show": "Tonen",
|
||||
"size": "Grootte{suffix}",
|
||||
"upload": "Uploaden",
|
||||
"uploadSettingsChunked": "Max. aantal gelijktijdige uploads: {maxConcurrentUpload}, brokgrootte: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Max. aantal gelijktijdige uploads: {maxConcurrentUpload}, upload in brokken uitgeschakeld",
|
||||
"optionalPassword": "Optioneel wachtwoord",
|
||||
"optionalPasswordHelp": "Indien ingesteld, moeten gebruikers dit wachtwoord invoeren om de gedeelde inhoud te bekijken.",
|
||||
"typeName": "Type:",
|
||||
"deleted": "Met succes verwijderd!",
|
||||
"conflictsDetected": "Conflicten gedetecteerd",
|
||||
"copyPasteConfirm": "Bevestiging kopiëren/plakken",
|
||||
"copyItemQuestion": "Dit item kopiëren naar de nieuwe locatie?",
|
||||
"moveItemQuestion": "Dit item verplaatsen naar de nieuwe locatie?",
|
||||
"sourcePath": "Bron:",
|
||||
"destinationPath": "Bestemming:",
|
||||
"closeWithActiveUploads": "Actieve uploads",
|
||||
"closeWithActiveUploadsMessage": "Waarschuwing: U hebt actieve uploads en het sluiten van het uploadvenster kan resulteren in onvoltooide uploads. Wat wilt u doen?",
|
||||
"basicInfo": "Basisinformatie",
|
||||
"directoryInfo": "Mapinformatie",
|
||||
"additionalInfo": "Aanvullende informatie",
|
||||
"hidden": "Verborgen",
|
||||
"hasPreview": "Voorbeeld",
|
||||
"checksums": "Controlesommen",
|
||||
"hashAlgorithm": "Hash-algoritme",
|
||||
"hashValue": "Hashwaarde",
|
||||
"selectHashAlgorithm": "Selecteer een hash-algoritme",
|
||||
"generate": "Genereren",
|
||||
"errorGeneratingHash": "Fout bij het genereren van hash",
|
||||
"hashCopied": "Hash gekopieerd naar klembord",
|
||||
"copyToClipboardFailed": "Kan niet naar klembord kopiëren",
|
||||
"errorCopyingHash": "Fout bij het kopiëren van hash",
|
||||
"token": "Token",
|
||||
"onlyOfficeId": "OnlyOffice-ID",
|
||||
"firstLoadTitle": "Welkom bij FileBrowser Quantum!",
|
||||
"firstLoadBody": "Er is een nieuwe database aangemaakt bij het opstarten. Als dit niet is wat u verwachtte, controleer dan het configuratiebestand voor het `server.database` pad en zorg ervoor dat het bestaat. Op docker kan het databasepad `./data/database.db` of `./database.db` zijn.",
|
||||
"halloweenTitle": "Happy Halloween!",
|
||||
"halloweenBody": "Het Halloween-thema is toegepast. U kunt het uitschakelen als u dit niet prettig vindt, of permanent uitschakelen in de profielinstellingen door een ander thema te selecteren.",
|
||||
"moveFailed": "Verplaatsen mislukt",
|
||||
"operationFailed": "Bewerking is mislukt",
|
||||
"partialSuccess": "Enkele bewerkingen zijn mislukt: {succeeded} gelukt, {failed} mislukt."
|
||||
},
|
||||
"search": {
|
||||
"searchContext": "Zoekcontext: {context}",
|
||||
"onlyFolders": "Alleen mappen",
|
||||
"onlyFiles": "Alleen bestanden",
|
||||
"showOptions": "Opties weergeven",
|
||||
"notEnoughCharacters": "Niet genoeg tekens om te zoeken (min {minSearchLength})",
|
||||
"typeToSearch": "Begin met het typen van {minSearchLength} of meer tekens om te beginnen met zoeken.",
|
||||
"noResults": "Geen resultaten gevonden in geïndexeerde zoekopdrachten.",
|
||||
"smallerThan": "Kleiner dan:",
|
||||
"largerThan": "Groter dan:",
|
||||
"showPreviewImages": "Voorbeeldafbeeldingen tonen",
|
||||
"showPreviewImagesDescription": "Toon, indien beschikbaar, miniatuurvoorbeelden voor zoekresultaten en toon grotere pictogrammen voor alle resultaten.",
|
||||
"helpText1": "Er wordt gezocht op elk teken dat u 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."
|
||||
},
|
||||
"profileSettings": {
|
||||
"previewFolder": "Toon miniaturen voor mappen indien beschikbaar",
|
||||
"previewFolderDescription": "Toon miniatuurafbeeldingen voor mappen met het eerste item met miniatuurafbeelding als omslag.",
|
||||
"debugOfficeEditor": "OnlyOffice foutopsporingsmodus inschakelen",
|
||||
"debugOfficeEditorDescription": "Een foutopsporingspop-up weergeven met aanvullende informatie in de OnlyOffice-editor.",
|
||||
"defaultMediaPlayer": "De ingebouwde mediaspeler van uw browser gebruiken",
|
||||
"defaultMediaPlayerDescription": "Gebruik de eigen mediaspeler van uw browser voor video- en audiobestanden in plaats van de meegeleverde 'vue-plyr' mediaspeler van FileBrowser.",
|
||||
"showSelectMultiple": "Meervoudige selectie 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.",
|
||||
"customTheme": "Kies uw thema",
|
||||
"defaultThemeDescription": "standaard - Het standaardthema",
|
||||
"setDateFormat": "Exact datumformaat instellen",
|
||||
"showHiddenFiles": "Verborgen bestanden weergeven",
|
||||
"showHiddenFilesDescription": "Toon verborgen bestanden in de lijst (bijv. .thumbnails, ProgramData), maar ze zijn nog steeds handmatig toegankelijk als ze zijn geconfigureerd om te worden geïndexeerd.",
|
||||
"showQuickDownload": "Downloadpictogram weergeven voor snelle toegang",
|
||||
"showQuickDownloadDescription": "Toon een downloadpictogram voor snelle toegang tot elk bestandsitem waarop kan worden geklikt om het meteen te downloaden.",
|
||||
"previewImages": "Miniaturen van afbeeldingen weergeven",
|
||||
"previewImagesDescription": "Toon miniatuurvoorbeelden voor afbeeldingen, toont generiek pictogram indien uitgeschakeld.",
|
||||
"previewVideos": "Miniaturen van video's weergeven",
|
||||
"previewVideosDescription": "Toon miniatuurvoorbeelden voor video's, toont generiek pictogram indien uitgeschakeld.",
|
||||
"previewMotionVideos": "Bewegingsvoorbeeld voor video's inschakelen",
|
||||
"previewMotionVideosDescription": "Bewegingsvoorbeeld is het geanimeerde popup-voorbeeld dat door de frames van de video loopt.",
|
||||
"highQualityPreview": "Miniaturen van hoge kwaliteit",
|
||||
"highQualityPreviewDescription": "Heeft alleen invloed op de galerieweergave. Het laden kan langer duren bij een langzame verbinding.",
|
||||
"editOffice": "Office-bestanden bekijken en bewerken met OnlyOffice",
|
||||
"editOfficeDescription": "Als alleen office is ingeschakeld, is de bestandsviewer/editor uitgeschakeld voor alle bestandsextensies die gerelateerd zijn aan Office-bestanden.",
|
||||
"popupPreview": "Pop-upvoorbeeld inschakelen wanneer u met de muis over bestanden beweegt",
|
||||
"popupPreviewDescription": "Er verschijnt een overlay pop-upvenster over de UI wanneer u met de muis over bestanden beweegt die een groot voorbeeld ondersteunen.",
|
||||
"filePreviewOptions": "Opties voor bestandsvoorbeeld",
|
||||
"disableUpdateNotifications": "Het tonen van meldingen update uitschakelen",
|
||||
"disableUpdateNotificationsDescription": "Schakel de banner met updatemeldingen uit die wordt weergegeven voor beheerders in de zijbalk.",
|
||||
"disableHideSidebar": "Houd de zijbalk open bij het bekijken of bewerken van bestanden",
|
||||
"disableHideSidebarDescription": "Als deze optie is ingeschakeld, wordt de zijbalk niet automatisch verborgen bij het bekijken of bewerken van bestanden.",
|
||||
"autoplayMedia": "Mediabestanden automatisch afspelen bij het bekijken",
|
||||
"autoplayMediaDescription": "Mediabestanden automatisch afspelen bij het bekijken. Dit werkt alleen als het bestand een mediabestand is.",
|
||||
"sidebarOptions": "Zijbalk-opties",
|
||||
"disableQuickToggles": "Snel wisselen uitschakelen",
|
||||
"disableFileExtensions": "Voer bestandsextensies in om uit te schakelen",
|
||||
"disableQuickTogglesDescription": "Schakel de sneltoetsen uit die op de zijbalk worden weergegeven.",
|
||||
"disableSearchOptions": "Zoekopties uitschakelen",
|
||||
"disableSearchOptionsDescription": "Schakel de zoekopties in de zijbalk uit.",
|
||||
"hideSidebarFileActions": "Verberg de knop Bestandsacties in de zijbalk",
|
||||
"deleteWithoutConfirming": "Bestanden verwijderen zonder bevestigingsvenster",
|
||||
"deleteWithoutConfirmingDescription": "Bestanden verwijderen zonder bevestigingsvenster. Dit werkt alleen als het bestand een mediabestand is.",
|
||||
"editorViewerOptions": "Opties voor editor en viewer",
|
||||
"editorQuickSave": "Snel opslaan in editor",
|
||||
"editorQuickSaveDescription": "De teksteditor toont altijd het pictogram voor opslaan en verbergt alle andere acties",
|
||||
"previewOffice": "Miniaturen weergeven voor Office-bestanden",
|
||||
"previewOfficeDescription": "Toon miniaturen voor kantoorbestandstypen in de listingweergave",
|
||||
"disableThumbnailPreviews": "Miniaturen uitschakelen voor bepaalde bestandsextensies",
|
||||
"disableThumbnailPreviewsDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor miniatuurafbeeldingen uitgeschakeld moeten worden (bijv. '.txt .html').",
|
||||
"disableViewingFiles": "Bestandsviewer/editor uitschakelen voor bepaalde bestandsextensies",
|
||||
"disableViewingFilesDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor de bestandsviewer/editor uitgeschakeld moet worden (bijv. '.txt .pdf').",
|
||||
"disablePreviewExt": "Voorbeelden van Office-bestanden uitschakelen voor bepaalde bestandsextensies",
|
||||
"disablePreviewExtDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor Office-bestandsvoorvertoningen moeten worden uitgeschakeld (bijv. '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Een door spaties gescheiden lijst van bestandsextensies waarvoor de OnlyOffice editor en viewer uitgeschakeld moeten worden - of '*' om alle extensies uit te schakelen. (bijv. '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Office Viewer uitschakelen voor bepaalde bestandsextensies",
|
||||
"fileViewerOptions": "Opties voor bestandsviewer",
|
||||
"themeAndLanguage": "Thema & Taal"
|
||||
},
|
||||
"settings": {
|
||||
"systemAdmin": "Systeem & Beheer",
|
||||
"configViewer": "Configuratiescherm",
|
||||
"configViewerShowFull": "Volledige configuratie weergeven",
|
||||
"configViewerShowComments": "Opmerkingen tonen",
|
||||
"configViewerLoadConfig": "Configuratie laden",
|
||||
"enterPassword": "Wachtwoord invoeren",
|
||||
"enterPasswordAgain": "Wachtwoord opnieuw invoeren",
|
||||
"passwordsDoNotMatch": "Wachtwoorden komen niet overeen",
|
||||
"loginMethod": "Aanmeldingsmethode",
|
||||
"loginMethods": {
|
||||
"password": "Wachtwoord",
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"loginMethodDescription": "toegestane aanmeldingsmethode voor de gebruiker.",
|
||||
"modifyOtherUser": "Gebruiker wijzigen:",
|
||||
"modifyCurrentUser": "Huidige gebruiker wijzigen",
|
||||
"themeColor": "Themakleur",
|
||||
"listingOptions": "Lijstopties",
|
||||
"searchOptions": "Zoekopties",
|
||||
"emptyGlobal": "Op dit moment zijn er geen globale configuratieopties. U kunt de configuratie yaml bewerken om systeeminstellingen aan te passen.",
|
||||
"scopes": "Toegestane bronnen en het gebruikersbereik daarbinnen definiëren",
|
||||
"UserManagement": "Gebruikersbeheer",
|
||||
"userNotAdmin": "Gebruiker is geen beheerder",
|
||||
"administrator": "Beheerder",
|
||||
"allowCommands": "Opdrachten uitvoeren",
|
||||
"allowEdit": "Bestanden of mappen bewerken, hernoemen of verwijderen",
|
||||
"allowNew": "Nieuwe bestanden of mappen aanmaken",
|
||||
"allowPublish": "Nieuwe berichten en pagina's publiceren",
|
||||
"allowSignup": "Gebruikers toestaan om zich te registreren",
|
||||
"avoidChanges": "(Leeg laten om wijzigingen te voorkomen)",
|
||||
"brandingDirectoryPath": "Pad naar map met Branding",
|
||||
"brandingHelp": "U kunt aanpassen hoe deze FileBrowser 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.\nMeer informatie over het aanpassen van de branding, kunt u lezen op {0}.",
|
||||
"changePassword": "Wachtwoord wijzigen",
|
||||
"commandRunner": "Opdrachten uitvoeren",
|
||||
"commandRunnerHelp": "Hier kunt u opdrachten instellen die worden uitgevoerd in de genoemde gebeurtenissen. U moet er één per regel schrijven. De omgevingsvariabelen {0} en {1} zijn beschikbaar, waarbij {0} relatief is ten opzichte van {1}. Lees de {2} voor meer informatie over deze functie en de beschikbare omgevingsvariabelen.",
|
||||
"commandsUpdated": "Opdrachten bijgewerkt!",
|
||||
"createUserDir": "Maak automatisch een thuismap aan wanneer een nieuwe gebruiker wordt aangemaakt",
|
||||
"tusUploads": "Verzamelde uploads",
|
||||
"tusUploadsHelp": "FileBrowser ondersteunt bestandsuploads in brokken, waardoor efficiënte, betrouwbare, hervattbare bestandsuploads mogelijk zijn, zelfs op onbetrouwbare netwerken.",
|
||||
"tusUploadsChunkSize": "Geeft de maximale grootte van een verzoek aan (directe uploads worden gebruikt voor kleinere uploads). U kunt een gewoon geheel getal invoeren dat staat voor een bytes-invoer of een tekenreeks zoals 10MB, 1GB enz.",
|
||||
"tusUploadsRetryCount": "Aantal pogingen dat moet worden uitgevoerd als het uploaden van een brok mislukt.",
|
||||
"userHomeBasePath": "Basispad voor thuismappen van gebruikers",
|
||||
"userScopeGenerationPlaceholder": "Het bereik wordt automatisch gegenereerd",
|
||||
"createUserHomeDirectory": "Thuismap voor gebruiker aanmaken",
|
||||
"customStylesheet": "Aangepast Stylesheet",
|
||||
"defaultUserDescription": "Dit zijn de standaardinstellingen voor nieuwe gebruikers.",
|
||||
"disableExternalLinks": "Externe links uitschakelen (behalve documentatie)",
|
||||
"disableUsedDiskPercentage": "Grafiek voor gebruikt schijfpercentage uitschakelen",
|
||||
"executeOnShell": "Uitvoeren in het opdrachtvenster",
|
||||
"executeOnShellDescription": "FileBrowser voert de opdrachten standaard uit door hun binaire bestanden rechtstreeks op te roepen. Als u ze in plaats daarvan wilt uitvoeren op een opdrachtvenster (zoals Bash of PowerShell), kunt u dit hier definiëren met de vereiste argumenten en vlaggen. Indien ingesteld, wordt de opdracht die u uitvoert, toegevoegd als een argument. Dit is van toepassing op zowel gebruikersopdrachten als event hooks.",
|
||||
"globalRules": "Dit is een algemene reeks regels die al dan niet zijn toegestaan. Ze zijn van toepassing op elke gebruiker. U kunt specifieke regels voor de instellingen van elke gebruiker definiëren om deze te overschrijven.",
|
||||
"globalSettings": "Algemene instellingen",
|
||||
"shareSettings": "Instellingen voor Delen",
|
||||
"api": "API-sleutels",
|
||||
"insertPath": "Een pad invoeren",
|
||||
"insertRegex": "Regex expressie invoeren",
|
||||
"instanceName": "Naam van de instantie",
|
||||
"lockPassword": "Voorkom dat de gebruiker het wachtwoord wijzigt",
|
||||
"newPassword": "Uw nieuwe wachtwoord",
|
||||
"newPasswordConfirm": "Uw nieuwe wachtwoord bevestigen",
|
||||
"newUser": "Nieuwe gebruiker",
|
||||
"newUserHintSubFolder": "bijv. '/submap' - leeg voor standaard",
|
||||
"passwordUpdated": "Wachtwoord bijgewerkt!",
|
||||
"permissions": {
|
||||
"admin": "Beheerder",
|
||||
"modify": "Bestanden bewerken",
|
||||
"share": "Bestanden delen",
|
||||
"api": "Langlevende API-sleutels aanmaken en beheren",
|
||||
"realtime": "Real-time verbindingen en updates mogelijk maken",
|
||||
"create": "Bestanden aanmaken",
|
||||
"delete": "Bestanden verwijderen",
|
||||
"download": "Bestanden downloaden"
|
||||
},
|
||||
"permissions-name": "Machtigingen",
|
||||
"permissionsHelp": "Gebruikersrechten bepalen welke acties de gebruiker kan uitvoeren. Toegang als beheerder geeft volledige controle om de instellingen van elke gebruiker bij te werken en te wijzigen, evenals vele systeeminstellingen.",
|
||||
"profileSettings": "Profielinstellingen",
|
||||
"ruleExample1": "voorkomt de toegang tot elk puntbestand (zoals .git, .gitignore) in elke map.\n",
|
||||
"ruleExample2": "blokkeert de toegang tot het bestand met de naam Caddyfile in de hoofdmap van het bereik.",
|
||||
"rulesHelp": "Hier kunt u een reeks regels voor toestaan en niet toestaan voor deze specifieke gebruiker definiëren. De geblokkeerde bestanden verschijnen niet in de lijsten en zijn niet toegankelijk voor de gebruiker. We ondersteunen regex en paden relatief ten opzichte van het bereik van gebruikers. \n",
|
||||
"settingsUpdated": "Instellingen bijgewerkt!",
|
||||
"shareManagement": "Beheer van Gedeelde mappen",
|
||||
"shareDeleted": "Gedeelde map verwijderd!",
|
||||
"singleClick": "Gebruik één klik om bestanden en mappen te openen",
|
||||
"userCommandsHelp": "Een lijst met beschikbare opdrachten voor de gebruiker gescheiden door spaties. Voorbeeld: \n",
|
||||
"userCreated": "Gebruiker aangemaakt!",
|
||||
"userDefaults": "Standaardinstellingen gebruiker",
|
||||
"userDeleted": "Gebruiker verwijderd!",
|
||||
"userManagement": "Gebruikersbeheer",
|
||||
"userUpdated": "Gebruiker bijgewerkt!",
|
||||
"adminOptions": "Opties voor beheerders"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Site-instellingen",
|
||||
"updateIsAvailable": "Er is een update beschikbaar!",
|
||||
"fileActions": "Bestandsacties",
|
||||
"customizeLinks": "Zijbalk koppelingen aanpassen",
|
||||
"customizeLinksDescription": "Pas de koppelingen in de zijbalk aan. Gebruik slepen en neerzetten om te ordenen.",
|
||||
"customizeShareLinks": "Koppelingen in Zijbalk Delen aanpassen",
|
||||
"customizeShareLinksDescription": "Pas de koppelingen aan die worden weergegeven in de zijbalk voor deze gedeelde map. Slepen en neerzetten om te herschikken.",
|
||||
"currentLinks": "Huidige koppelingen",
|
||||
"addNewLink": "Nieuwe koppeling toevoegen",
|
||||
"editLink": "Koppeling bewerken",
|
||||
"linkType": "Type koppeling",
|
||||
"selectLinkType": "Selecteer een type koppeling",
|
||||
"customLink": "Aangepaste koppeling",
|
||||
"selectSource": "Bron selecteren",
|
||||
"chooseSource": "Kies een bron",
|
||||
"selectShare": "Selecteer gedeelde map",
|
||||
"chooseShare": "Kies een gedeelde map",
|
||||
"selectSharePath": "Pad binnen gedeelde map (optioneel)",
|
||||
"sharePathPlaceholder": "bijv. /submap of leeg laten voor root",
|
||||
"selectTool": "Selecteer gereedschap",
|
||||
"chooseTool": "Kies een hulpmiddel",
|
||||
"linkName": "Link Naam",
|
||||
"linkNamePlaceholder": "Naam koppeling invoeren",
|
||||
"linkUrl": "URL van koppeling",
|
||||
"linkUrlPlaceholder": "Voer een URL of pad in (bijv. /files/path of https://example.com)",
|
||||
"linkIcon": "Pictogram van koppeling",
|
||||
"linkIconPlaceholder": "Geef optioneel een materiaalpictogramnaam op",
|
||||
"browseIcons": "Bladeren door pictogrammen",
|
||||
"pickIcon": "Kies een pictogram",
|
||||
"pickIconDescription": "Zoek en selecteer een pictogram uit de bibliotheek met materiaalpictogrammen",
|
||||
"linksUpdatedSuccess": "Koppelingen in zijbalk succesvol bijgewerkt!",
|
||||
"shareLinksUpdatedSuccess": "Koppelingen in zijbalk Delen succesvol bijgewerkt!",
|
||||
"linksUpdateFailed": "Bijwerken van koppelingen in zijbalk is mislukt",
|
||||
"sourceNotAccessible": "Deze bron is niet langer toegankelijk voor jou",
|
||||
"noLinksYet": "Er zijn nog geen links geconfigureerd. Voeg hieronder uw eerste link toe.",
|
||||
"shareHome": "Thuis delen",
|
||||
"dividerNamePlaceholder": "Scheidingstekst (leeg laten om een lijn weer te geven)"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Koppeling gekopieerd!"
|
||||
},
|
||||
"time": {
|
||||
"days": "Dagen",
|
||||
"hours": "Uren",
|
||||
"minutes": "Minuten",
|
||||
"seconds": "Seconden",
|
||||
"unit": "Tijdseenheid"
|
||||
},
|
||||
"api": {
|
||||
"title": "API-sleutels",
|
||||
"createdAt": "Aangemaakt op:",
|
||||
"expiresAt": "Verloopt op:",
|
||||
"permissions": "Machtigingen:",
|
||||
"createTitle": "API-sleutel aanmaken",
|
||||
"keyNamePlaceholder": "Voer een unieke naam voor de API-sleutel in:",
|
||||
"tokenDuration": "Duur van token:",
|
||||
"durationNumberPlaceholder": "Aantal",
|
||||
"days": "Dagen",
|
||||
"months": "Maanden",
|
||||
"permissionNote": "Kies minstens één machtiging voor de sleutel. Uw gebruiker moet deze rechten ook hebben.",
|
||||
"createKeyFailed": "API-sleutel is niet aangemaakt.",
|
||||
"createKeySuccess": "API-sleutel met succes aangemaakt.",
|
||||
"apiKeyDeleted": "API-sleutel verwijderd!",
|
||||
"description": "API-sleutels zijn gebaseerd op de gebruiker die ze aanmaakt. Zie",
|
||||
"swaggerLinkText": "branie-pagina",
|
||||
"created": "Aangemaakt op",
|
||||
"expires": "Verloopt op",
|
||||
"actions": "Acties",
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"tokenType": "Type token:",
|
||||
"minimalDescription": "Minimaal token",
|
||||
"minimalInfo": "Sluit tokenaanpassingen uit om een kleinere tokenlengte te creëren. Exacte overeenkomst met de rechten van de bijbehorende gebruiker.",
|
||||
"clickToCopyKey": "Klik om sleutel te kopiëren (verborgen)"
|
||||
},
|
||||
"index": {
|
||||
"unknown": "Onbekend",
|
||||
"simple": "Eenvoudig",
|
||||
"normal": "Normaal",
|
||||
"complex": "Complex",
|
||||
"highlyComplex": "Zeer complex",
|
||||
"assessment": "Beoordeling",
|
||||
"ready": "Klaar",
|
||||
"indexing": "Indexering",
|
||||
"unavailable": "Niet beschikbaar",
|
||||
"error": "Fout",
|
||||
"lastScanned": "Laatst gescand",
|
||||
"quickScan": "Seconden voor Snelle scan",
|
||||
"fullScan": "Seconden voor Volledige scan",
|
||||
"settingsHover": "Instellingen voor gebruiker",
|
||||
"toggleClick": "Enkele klik schakelen",
|
||||
"toggleDark": "Donkere modus schakelen",
|
||||
"toggleSticky": "Kleefmodus schakelen",
|
||||
"noSources": "Geen bronnen beschikbaar voor deze gebruiker. Voeg een bron toe voor deze gebruiker of configureer een bron met defaultEnabled: true"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Twee-factorenauthenticatie",
|
||||
"verifyInstructions": "Voer de code van uw authenticator-app in",
|
||||
"generate": "Scan de QR-code met uw authenticator-app",
|
||||
"codeInputPlaceholder": "6-cijferige code",
|
||||
"invalidCodeType": "De verificatiecode moet uit 6 engelse cijfers bestaan",
|
||||
"enabledSuccessfully": "OTP succesvol ingeschakeld",
|
||||
"verificationFailed": "Verificatie is mislukt",
|
||||
"verificationSucceed": "Verificatie is geslaagd",
|
||||
"generationFailed": "Aanmaken is mislukt"
|
||||
},
|
||||
"access": {
|
||||
"all": "Alles weigeren",
|
||||
"rules": "Toegangsregels",
|
||||
"deny": "Weigeren",
|
||||
"deniedUsers": "Geweigerde gebruikers",
|
||||
"deniedGroups": "Geweigerde groepen",
|
||||
"allowedUsers": "Toegestane gebruikers",
|
||||
"allowedGroups": "Toegestane groepen",
|
||||
"totalDenied": "Geweigerd",
|
||||
"totalAllowed": "Toegestaan",
|
||||
"allow": "Toestaan",
|
||||
"allowDeny": "Toestaan/weigeren",
|
||||
"userGroup": "Gebruiker/Groep",
|
||||
"enterName": "Voer de naam van de gebruiker of groep in",
|
||||
"deleted": "Toegangsregel verwijderd!",
|
||||
"deletedCascade": "Toegangsregels verwijderd van pad en alle subpaden!",
|
||||
"added": "Toegangsregel toegevoegd!",
|
||||
"accessManagement": "Toegangsbeheer",
|
||||
"defaultBehavior": "Standaardactie{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.",
|
||||
"cascadeDelete": "Recursief verwijderen",
|
||||
"cascadeDeleteDescription": "Indien ingeschakeld, zal het verwijderen van een toegangsregel deze ook verwijderen uit alle submappen. Hierdoor kunt u een gebruiker of groep volledig verwijderen uit de hele mappenstructuur."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Uploaden & Downloads",
|
||||
"maxConcurrentUpload": "Max. gelijktijdige uploads",
|
||||
"maxConcurrentUploadHelp": "Het maximum aantal gelijktijdige uploads. Hogere waarden presteren meestal beter, maar dit hangt van veel factoren af.",
|
||||
"uploadChunkSizeMb": "Upload brokgrootte in MB",
|
||||
"uploadChunkSizeMbHelp": "Gewoonlijk 5 tot 50 MB, kleiner is beter voor onstabiele of beperkte verbindingen. Instellen op 0 schakelt uploads in brokken uit.",
|
||||
"downloadChunkSizeMb": "Grootte downloadbrok in MB",
|
||||
"downloadChunkSizeMbHelp": "Brok-grootte voor downloads in brokken. Bestanden die groter zijn dan deze grootte worden in brokken gedownload om time-outproblemen te voorkomen (bijv. Cloudflare 524 fouten). Instellen op 0 schakelt downloads in brokken uit.",
|
||||
"clearAll": "Wissen voltooid moet fout, pauze en conflict verhelpen",
|
||||
"clearAllDescription": "De knop Wissen voltooid wist alle uploads in een voltooide status, inclusief fout-, gepauzeerde en conflictstatussen.",
|
||||
"uploadSettings": "Instellingen uploaden"
|
||||
},
|
||||
"colors": {
|
||||
"red": "rood",
|
||||
"green": "groen",
|
||||
"blue": "blauw",
|
||||
"yellow": "geel",
|
||||
"violet": "paars",
|
||||
"orange": "oranje"
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Het is niet gelukt de editor te initialiseren. Opnieuw 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.",
|
||||
"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.",
|
||||
"fileSaved": "Bestand opgeslagen!",
|
||||
"errorSavingFile": "Fout bij het opslaan van bestand: {error}"
|
||||
},
|
||||
"player": {
|
||||
"LoopEnabled": "Herhaling ingeschakeld",
|
||||
"LoopDisabled": "Herhaling uitgeschakeld",
|
||||
"PlayAllOncePlayback": "Alles eenmaal afspelen",
|
||||
"ShuffleAllPlayback": "Alles willekeurig afspelen",
|
||||
"PlayAllLoopedPlayback": "Alles herhaald afspelen",
|
||||
"QueueButtonHint": "Afspeellijst weergeven",
|
||||
"QueuePlayback": "Afspeellijst",
|
||||
"changePlaybackMode": "Afspeelmodus wijzigen",
|
||||
"changePlaybackModeHint": "Wijzig afspeelmodus om meer bestanden te zien",
|
||||
"emptyQueue": "Wachtrij is leeg"
|
||||
},
|
||||
"fileSizeAnalyzer": {
|
||||
"title": "Bestandsgrootte analyseren",
|
||||
"searchLocation": "Zoeklocatie",
|
||||
"selectPath": "Zoeklocatie selecteren",
|
||||
"largerThan": "Groter dan (MB)",
|
||||
"includeFolders": "Mappen opnemen",
|
||||
"includeFoldersDescription": "mappen meenemen in de analyse",
|
||||
"totalFiles": "Totaal aantal bestanden{suffix}",
|
||||
"totalSize": "Totale grootte{suffix}",
|
||||
"emptyState": "Voer zoekcriteria in en klik op Analyseren om bestandsgroottes te visualiseren",
|
||||
"completeResults": "Volledige resultaten getoond!",
|
||||
"incompleteResults": "Resultaten kunnen onvolledig zijn",
|
||||
"relativePercentage": "Relatief percentage{suffix}",
|
||||
"percentageOfResults": "{percentage}% resultaten"
|
||||
},
|
||||
"fileTypes": {
|
||||
"video": "Video",
|
||||
"image": "Afbeelding",
|
||||
"audio": "Audio",
|
||||
"archive": "Archief",
|
||||
"document": "Document",
|
||||
"binary": "Binair",
|
||||
"directory": "Map",
|
||||
"other": "Overig"
|
||||
},
|
||||
"tools": {
|
||||
"title": "Gereedschap",
|
||||
"description": "Hulpprogramma's en analysetools voor uw bestanden",
|
||||
"sizeAnalyzer": {
|
||||
"name": "Bestandsgrootte analyseren",
|
||||
"description": "Visualiseer grote bestanden en mappen met een interactieve treemap"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"name": "Duplicaatbestanden zoeken",
|
||||
"description": "Dubbele bestanden vinden en beheren om opslagruimte terug te winnen"
|
||||
},
|
||||
"materialIconPicker": {
|
||||
"name": "Material pictogrammenkiezer",
|
||||
"description": "Bladeren en zoeken in Material pictogrammen met live voorbeeld",
|
||||
"searchPlaceholder": "Pictogrammen zoeken...",
|
||||
"browseFullLibrary": "Blader door de volledige pictogrammenbibliotheek",
|
||||
"iconLibraryUrl": "fonts.google.com/icons",
|
||||
"popularIcons": "{count} populaire pictogrammen weergeven",
|
||||
"showingResults": "{count} resultaten weergeven",
|
||||
"noResults": "Geen pictogrammen gevonden",
|
||||
"tryDifferentSearch": "Probeer een andere zoekterm",
|
||||
"copiedToClipboard": "'{name}' is gekopieerd naar klembord!",
|
||||
"copyFailed": "Kopiëren naar klembord mislukt",
|
||||
"custom": "Aangepast",
|
||||
"useCustomIcon": "Gebruik een aangepast pictogram: {name}"
|
||||
},
|
||||
"fileWatcher": {
|
||||
"name": "Bestandenbewaker",
|
||||
"description": "Bestanden in realtime bewaken en volgen met latentiebewaking",
|
||||
"file": "Bestand{suffix}",
|
||||
"interval": "Update-interval",
|
||||
"stopWatching": "Stoppen met bewaken",
|
||||
"emptyState": "Kies een bestand en klik op afspelen om te beginnen met het controleren van bestandswijzigingen",
|
||||
"chooseFile": "Kies een bestand"
|
||||
},
|
||||
"toolNotFound": "Gereedschap niet gevonden",
|
||||
"backToTools": "Terug naar Gereedschap"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Meldingen",
|
||||
"description": "Alle meldingen van uw huidige sessie bekijken",
|
||||
"empty": "Nog geen meldingen",
|
||||
"clearAll": "Alles wissen",
|
||||
"clearConfirm": "Weet u zeker dat u de gehele meldingsgeschiedenis wilt wissen?",
|
||||
"total": "{count} meldingen"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "Duplicaatbestanden zoeken",
|
||||
"minSize": "Minimale grootte (MB){suffix}",
|
||||
"minSizeHint": "Alleen bestanden analyseren die groter zijn dan deze grootte (in megabytes)",
|
||||
"useChecksums": "Bestandsinhoud vergelijken",
|
||||
"useChecksumsDescription": "Neem ook de inhoud van bestanden op in de vergelijking. Zonder dit worden bestanden vergeleken op basis van grootte en bestandsnaam overeenkomst.",
|
||||
"findDuplicates": "Duplicaten zoeken",
|
||||
"groupsFound": "Dubbele groepen gevonden{suffix}",
|
||||
"totalWastedSpace": "Totale verspilde ruimte{suffix}",
|
||||
"wasted": "Verspild{suffix}",
|
||||
"emptyState": "Voer zoekcriteria in en klik op Duplicaten zoeken om dubbele bestanden te identificeren"
|
||||
},
|
||||
"events": {
|
||||
"serverShutdown": "De server is uitgeschakeld.",
|
||||
"authenticationFailed": "Authenticatie is mislukt. Ververs de pagina om opnieuw in te loggen.",
|
||||
"reconnected": "Opnieuw verbinding gemaakt met server.",
|
||||
"connectionLost": "De verbinding met de server is verbroken. Probeer opnieuw verbinding te maken..."
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Przejdź do pozycji",
|
||||
"watchFile": "Oglądaj plik",
|
||||
"showMore": "Pokaż opcje zaawansowane",
|
||||
"showLess": "Ukryj opcje zaawansowane",
|
||||
"goToSource": "Przejdź do lokalizacji źródłowej"
|
||||
"showLess": "Ukryj opcje zaawansowane"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Pobierz plik",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Utwórz konto",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Opcje użytkownika administratora",
|
||||
"searchOptions": "Opcje wyszukiwania",
|
||||
"systemAdmin": "System i administrator",
|
||||
"configViewer": "Przeglądarka konfiguracji",
|
||||
"configViewerShowFull": "Pokaż pełną konfigurację",
|
||||
"configViewerShowComments": "Pokaż komentarze",
|
||||
"configViewerLoadConfig": "Konfiguracja ładowania",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Pełnomocnik"
|
||||
},
|
||||
"newUserHintSubFolder": "np. '/subfolder' - domyślnie puste",
|
||||
"permissionsHelp": "Uprawnienia użytkownika kontrolują dostęp do rodzaju działań, które użytkownik może wykonywać. Dostęp administratora daje pełną kontrolę nad aktualizacją i zmianą ustawień dowolnego użytkownika, a także wielu ustawień systemowych.",
|
||||
"configViewer": "Przeglądarka konfiguracji"
|
||||
"permissionsHelp": "Uprawnienia użytkownika kontrolują dostęp do rodzaju działań, które użytkownik może wykonywać. Dostęp administratora daje pełną kontrolę nad aktualizacją i zmianą ustawień dowolnego użytkownika, a także wielu ustawień systemowych."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Ustawienia Strony",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Wyszukaj i wybierz ikonę z biblioteki Material Icons",
|
||||
"shareLinksUpdatedSuccess": "Linki do paska bocznego zostały pomyślnie zaktualizowane!",
|
||||
"shareHome": "Share Home",
|
||||
"linkIconPlaceholder": "Opcjonalnie można określić nazwę ikony materiału",
|
||||
"dividerNamePlaceholder": "Tekst podziału (pozostaw pusty, aby wyświetlić linię)"
|
||||
"linkIconPlaceholder": "Opcjonalnie można określić nazwę ikony materiału"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link Skopiowany!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Linie{suffix}",
|
||||
"lastUpdate": "Ostatnia aktualizacja{suffix}",
|
||||
"latency": "Opóźnienie{suffix}",
|
||||
"all": "Wszystkie{suffix}",
|
||||
"expiration": "Wygaśnięcie{suffix}",
|
||||
"divider": "Dzielnik{suffix}"
|
||||
"all": "Wszystkie{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Zasady dostępu",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Ir para o item",
|
||||
"watchFile": "Assista ao arquivo",
|
||||
"showMore": "Mostrar opções avançadas",
|
||||
"showLess": "Ocultar opções avançadas",
|
||||
"goToSource": "Ir para o local de origem"
|
||||
"showLess": "Ocultar opções avançadas"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Baixar arquivo",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Criar uma conta",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Opções do usuário administrador",
|
||||
"searchOptions": "Opções de pesquisa",
|
||||
"systemAdmin": "Sistema e administração",
|
||||
"configViewer": "Visualizador de configuração",
|
||||
"configViewerShowFull": "Mostrar configuração completa",
|
||||
"configViewerShowComments": "Mostrar comentários",
|
||||
"configViewerLoadConfig": "Configuração de carga",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "Por exemplo, '/subfolder' - em branco para o padrão",
|
||||
"permissionsHelp": "As permissões de usuário controlam o acesso aos tipos de ações que o usuário pode executar. O acesso de administrador dá controle total para atualizar e alterar as configurações de qualquer usuário, bem como muitas configurações do sistema.",
|
||||
"configViewer": "Visualizador de configuração"
|
||||
"permissionsHelp": "As permissões de usuário controlam o acesso aos tipos de ações que o usuário pode executar. O acesso de administrador dá controle total para atualizar e alterar as configurações de qualquer usuário, bem como muitas configurações do sistema."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Configurações do site",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Pesquise e selecione um ícone na biblioteca de ícones de materiais",
|
||||
"shareLinksUpdatedSuccess": "Os links da barra lateral de compartilhamento foram atualizados com sucesso!",
|
||||
"shareHome": "Compartilhar casa",
|
||||
"linkIconPlaceholder": "Opcionalmente, especifique um nome de ícone de material",
|
||||
"dividerNamePlaceholder": "Texto do divisor (deixe em branco para mostrar uma linha)"
|
||||
"linkIconPlaceholder": "Opcionalmente, especifique um nome de ícone de material"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link copiado!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Linhas{suffix}",
|
||||
"lastUpdate": "Última atualização{suffix}",
|
||||
"latency": "Latência{suffix}",
|
||||
"all": "Todos{suffix}",
|
||||
"expiration": "Vencimento{suffix}",
|
||||
"divider": "Divisor{suffix}"
|
||||
"all": "Todos{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regras de acesso",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Ir para o item",
|
||||
"watchFile": "Ver ficheiro",
|
||||
"showMore": "Mostrar opções avançadas",
|
||||
"showLess": "Ocultar opções avançadas",
|
||||
"goToSource": "Ir para o local de origem"
|
||||
"showLess": "Ocultar opções avançadas"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarregar ficheiro",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Criar uma conta",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Opções do utilizador administrador",
|
||||
"searchOptions": "Opções de pesquisa",
|
||||
"systemAdmin": "Sistema e administração",
|
||||
"configViewer": "Visualizador de configuração",
|
||||
"configViewerShowFull": "Mostrar configuração completa",
|
||||
"configViewerShowComments": "Mostrar comentários",
|
||||
"configViewerLoadConfig": "Configuração de carga",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "por exemplo, '/subpasta' - em branco por defeito",
|
||||
"permissionsHelp": "As permissões de utilizador controlam o acesso ao tipo de acções que o utilizador pode executar. O acesso de administrador dá controlo total para atualizar e alterar as definições de qualquer utilizador, bem como muitas definições do sistema.",
|
||||
"configViewer": "Visualizador de configuração"
|
||||
"permissionsHelp": "As permissões de utilizador controlam o acesso ao tipo de acções que o utilizador pode executar. O acesso de administrador dá controlo total para atualizar e alterar as definições de qualquer utilizador, bem como muitas definições do sistema."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Configurações do site",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Procurar e selecionar um ícone na biblioteca de ícones de materiais",
|
||||
"shareLinksUpdatedSuccess": "As ligações da barra lateral de partilha foram actualizadas com êxito!",
|
||||
"shareHome": "Partilhar casa",
|
||||
"linkIconPlaceholder": "Especificar opcionalmente um nome de ícone de material",
|
||||
"dividerNamePlaceholder": "Texto do separador (deixar em branco para mostrar uma linha)"
|
||||
"linkIconPlaceholder": "Especificar opcionalmente um nome de ícone de material"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link copiado!"
|
||||
@@ -708,9 +705,7 @@
|
||||
"lines": "Linhas{suffix}",
|
||||
"lastUpdate": "Última atualização{suffix}",
|
||||
"latency": "Latência{suffix}",
|
||||
"all": "Todos{suffix}",
|
||||
"expiration": "Prazo de validade{suffix}",
|
||||
"divider": "Divisor{suffix}"
|
||||
"all": "Todos{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regras de acesso",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Du-te la articol",
|
||||
"watchFile": "Fișier de vizionare",
|
||||
"showMore": "Afișare opțiuni avansate",
|
||||
"showLess": "Ascundeți opțiunile avansate",
|
||||
"goToSource": "Du-te la locația sursei"
|
||||
"showLess": "Ascundeți opțiunile avansate"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarcă fișier",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Crează cont",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Opțiuni utilizator administrator",
|
||||
"searchOptions": "Opțiuni de căutare",
|
||||
"systemAdmin": "Sistem și administrare",
|
||||
"configViewer": "Vizualizator de configurare",
|
||||
"configViewerShowFull": "Afișați configurația completă",
|
||||
"configViewerShowComments": "Afișați comentariile",
|
||||
"configViewerLoadConfig": "Configurare încărcare",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "de exemplu \"/subfolder\" - gol pentru implicit",
|
||||
"permissionsHelp": "Permisiunile utilizatorului controlează accesul la ce tip de acțiuni poate efectua utilizatorul. Accesul de administrator oferă control deplin pentru actualizarea și modificarea setărilor oricărui utilizator, precum și a multor setări de sistem.",
|
||||
"configViewer": "Vizualizator de configurare"
|
||||
"permissionsHelp": "Permisiunile utilizatorului controlează accesul la ce tip de acțiuni poate efectua utilizatorul. Accesul de administrator oferă control deplin pentru actualizarea și modificarea setărilor oricărui utilizator, precum și a multor setări de sistem."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Setări site",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Căutați și selectați o pictogramă din biblioteca Material Icons",
|
||||
"shareLinksUpdatedSuccess": "Link-urile din bara laterală de partajare au fost actualizate cu succes!",
|
||||
"shareHome": "Share Home",
|
||||
"linkIconPlaceholder": "Opțional, specificați un nume de pictogramă de material",
|
||||
"dividerNamePlaceholder": "Textul separatorului (lăsați gol pentru a afișa o linie)"
|
||||
"linkIconPlaceholder": "Opțional, specificați un nume de pictogramă de material"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Legătură copiată!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Linii{suffix}",
|
||||
"lastUpdate": "Ultima actualizare{suffix}",
|
||||
"latency": "Latență{suffix}",
|
||||
"all": "Toate{suffix}",
|
||||
"expiration": "Expirare{suffix}",
|
||||
"divider": "Divizor{suffix}"
|
||||
"all": "Toate{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Reguli de acces",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Перейти к пункту",
|
||||
"watchFile": "Смотреть файл",
|
||||
"showMore": "Показать дополнительные параметры",
|
||||
"showLess": "Скрыть дополнительные параметры",
|
||||
"goToSource": "Перейти к исходному местоположению"
|
||||
"showLess": "Скрыть дополнительные параметры"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Скачать файл",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Создать аккаунт",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Параметры пользователя администратора",
|
||||
"searchOptions": "Параметры поиска",
|
||||
"systemAdmin": "Система и администрирование",
|
||||
"configViewer": "Программа просмотра конфигурации",
|
||||
"configViewerShowFull": "Показать полную конфигурацию",
|
||||
"configViewerShowComments": "Показать комментарии",
|
||||
"configViewerLoadConfig": "Загрузить конфигурацию",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Прокси-сервер"
|
||||
},
|
||||
"newUserHintSubFolder": "например, '/subfolder' - пустое место по умолчанию",
|
||||
"permissionsHelp": "Разрешения пользователя контролируют, какие действия он может выполнять. Доступ администратора дает полный контроль над обновлением и изменением настроек любого пользователя, а также многих системных настроек.",
|
||||
"configViewer": "Программа просмотра конфигурации"
|
||||
"permissionsHelp": "Разрешения пользователя контролируют, какие действия он может выполнять. Доступ администратора дает полный контроль над обновлением и изменением настроек любого пользователя, а также многих системных настроек."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Настройки сайта",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Найдите и выберите значок в библиотеке Material Icons.",
|
||||
"shareLinksUpdatedSuccess": "Ссылки на боковой панели успешно обновлены!",
|
||||
"shareHome": "Поделиться Домой",
|
||||
"linkIconPlaceholder": "Опционально укажите имя значка материала",
|
||||
"dividerNamePlaceholder": "Текст разделителя (оставьте пустым, чтобы показать строку)"
|
||||
"linkIconPlaceholder": "Опционально укажите имя значка материала"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Ссылка скопирована!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Линии{suffix}",
|
||||
"lastUpdate": "Последнее обновление{suffix}",
|
||||
"latency": "Латентность{suffix}",
|
||||
"all": "Все{suffix}",
|
||||
"expiration": "Срок действия{suffix}",
|
||||
"divider": "Разделитель{suffix}"
|
||||
"all": "Все{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Правила доступа",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Prejsť na položku",
|
||||
"watchFile": "Sledovať súbor",
|
||||
"showMore": "Zobraziť rozšírené možnosti",
|
||||
"showLess": "Skryť rozšírené možnosti",
|
||||
"goToSource": "Prejsť na zdrojové miesto"
|
||||
"showLess": "Skryť rozšírené možnosti"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stiahnuť súbor",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Vytvoriť účet",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Možnosti používateľa správcu",
|
||||
"searchOptions": "Možnosti vyhľadávania",
|
||||
"systemAdmin": "Systém a administrátor",
|
||||
"configViewer": "Prehliadač konfigurácie",
|
||||
"configViewerShowFull": "Zobraziť úplnú konfiguráciu",
|
||||
"configViewerShowComments": "Zobraziť komentáre",
|
||||
"configViewerLoadConfig": "Konfigurácia načítania",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "napr. '/subfolder' - prázdne pre predvolené nastavenie",
|
||||
"permissionsHelp": "Oprávnenia používateľa riadia prístup k typom akcií, ktoré môže používateľ vykonávať. Prístup administrátora poskytuje úplnú kontrolu nad aktualizáciou a zmenou nastavení ktoréhokoľvek používateľa, ako aj nad mnohými systémovými nastaveniami.",
|
||||
"configViewer": "Prehliadač konfigurácie"
|
||||
"permissionsHelp": "Oprávnenia používateľa riadia prístup k typom akcií, ktoré môže používateľ vykonávať. Prístup administrátora poskytuje úplnú kontrolu nad aktualizáciou a zmenou nastavení ktoréhokoľvek používateľa, ako aj nad mnohými systémovými nastaveniami."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Nastavenia stránky",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Vyhľadajte a vyberte ikonu z knižnice Material Icons",
|
||||
"shareLinksUpdatedSuccess": "Zdieľanie bočného panela odkazy úspešne aktualizované!",
|
||||
"shareHome": "Zdieľať Domov",
|
||||
"linkIconPlaceholder": "Voliteľne zadajte názov ikony materiálu",
|
||||
"dividerNamePlaceholder": "Text oddeľovača (nechajte prázdny, aby sa zobrazil riadok)"
|
||||
"linkIconPlaceholder": "Voliteľne zadajte názov ikony materiálu"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Odkaz skopírovaný!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Riadky{suffix}",
|
||||
"lastUpdate": "Posledná aktualizácia{suffix}",
|
||||
"latency": "Latencia{suffix}",
|
||||
"all": "Všetky{suffix}",
|
||||
"expiration": "Uplynutie platnosti{suffix}",
|
||||
"divider": "Rozdeľovač{suffix}"
|
||||
"all": "Všetky{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Pravidlá prístupu",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Gå till artikel",
|
||||
"watchFile": "Titta på filen",
|
||||
"showMore": "Visa avancerade alternativ",
|
||||
"showLess": "Dölj avancerade alternativ",
|
||||
"goToSource": "Gå till källplatsen"
|
||||
"showLess": "Dölj avancerade alternativ"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Ladda ner fil",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Skapa ett konto",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Alternativ för administratörsanvändare",
|
||||
"searchOptions": "Sökalternativ",
|
||||
"systemAdmin": "System & Administration",
|
||||
"configViewer": "Konfig-visare",
|
||||
"configViewerShowFull": "Visa fullständig konfiguration",
|
||||
"configViewerShowComments": "Visa kommentarer",
|
||||
"configViewerLoadConfig": "Ladda konfig",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "t.ex. \"/undermapp\" - tom för standard",
|
||||
"permissionsHelp": "Användarbehörigheter styr vilka typer av åtgärder som användaren kan utföra. Administratörsbehörighet ger full kontroll över uppdatering och ändring av alla användarinställningar samt många systeminställningar.",
|
||||
"configViewer": "Konfig-visare"
|
||||
"permissionsHelp": "Användarbehörigheter styr vilka typer av åtgärder som användaren kan utföra. Administratörsbehörighet ger full kontroll över uppdatering och ändring av alla användarinställningar samt många systeminställningar."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "System inställningar",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Sök och välj en ikon från Material Icons-biblioteket",
|
||||
"shareLinksUpdatedSuccess": "Dela sidofältets länkar uppdaterades framgångsrikt!",
|
||||
"shareHome": "Dela hem",
|
||||
"linkIconPlaceholder": "Ange eventuellt ett namn på materialikonen",
|
||||
"dividerNamePlaceholder": "Delningstext (lämna tom för att visa en rad)"
|
||||
"linkIconPlaceholder": "Ange eventuellt ett namn på materialikonen"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Länk kopierad"
|
||||
@@ -708,9 +705,7 @@
|
||||
"lines": "Linjer{suffix}",
|
||||
"lastUpdate": "Senaste uppdatering{suffix}",
|
||||
"latency": "Fördröjning{suffix}",
|
||||
"all": "Alla{suffix}",
|
||||
"expiration": "Förfallodag{suffix}",
|
||||
"divider": "Uppdelare{suffix}"
|
||||
"all": "Alla{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regler för åtkomst",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Öğeye git",
|
||||
"watchFile": "Dosyayı izle",
|
||||
"showMore": "Gelişmiş Seçenekleri Göster",
|
||||
"showLess": "Gelişmiş Seçenekleri Gizle",
|
||||
"goToSource": "Kaynak Konuma Git"
|
||||
"showLess": "Gelişmiş Seçenekleri Gizle"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Dosyayı indir",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Bir hesap oluşturun",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Yönetici kullanıcı seçenekleri",
|
||||
"searchOptions": "Arama seçenekleri",
|
||||
"systemAdmin": "Sistem ve Yönetici",
|
||||
"configViewer": "Konfigürasyon Görüntüleyici",
|
||||
"configViewerShowFull": "Tam yapılandırmayı göster",
|
||||
"configViewerShowComments": "Yorumları göster",
|
||||
"configViewerLoadConfig": "Yük Konfigürasyonu",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "örneğin '/subfolder' - varsayılan için boş",
|
||||
"permissionsHelp": "Kullanıcı izinleri, kullanıcının ne tür eylemler gerçekleştirebileceğine erişimi kontrol eder. Yönetici erişimi, herhangi bir kullanıcının ayarlarının yanı sıra birçok sistem ayarını güncellemek ve değiştirmek için tam kontrol sağlar.",
|
||||
"configViewer": "Yapılandırma Görüntüleyici"
|
||||
"permissionsHelp": "Kullanıcı izinleri, kullanıcının ne tür eylemler gerçekleştirebileceğine erişimi kontrol eder. Yönetici erişimi, herhangi bir kullanıcının ayarlarının yanı sıra birçok sistem ayarını güncellemek ve değiştirmek için tam kontrol sağlar."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Site ayarları!",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Materyal Simgeleri kitaplığından bir simge arayın ve seçin",
|
||||
"shareLinksUpdatedSuccess": "Kenar çubuğu bağlantılarını paylaşma başarıyla güncellendi!",
|
||||
"shareHome": "Ev Paylaşımı",
|
||||
"linkIconPlaceholder": "İsteğe bağlı olarak bir malzeme simgesi adı belirtin",
|
||||
"dividerNamePlaceholder": "Bölücü metin (bir çizgi göstermek için boş bırakın)"
|
||||
"linkIconPlaceholder": "İsteğe bağlı olarak bir malzeme simgesi adı belirtin"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link kopyalandı!"
|
||||
@@ -628,9 +625,7 @@
|
||||
"lines": "Çizgiler{suffix}",
|
||||
"lastUpdate": "Son Güncelleme{suffix}",
|
||||
"latency": "Gecikme{suffix}",
|
||||
"all": "Tümü{suffix}",
|
||||
"expiration": "Son kullanma tarihi{suffix}",
|
||||
"divider": "Bölücü{suffix}"
|
||||
"all": "Tümü{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Erişim kuralları",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "Перейти до пункту",
|
||||
"watchFile": "Переглянути файл",
|
||||
"showMore": "Показати додаткові параметри",
|
||||
"showLess": "Приховати додаткові параметри",
|
||||
"goToSource": "Перейдіть до Розташування джерела"
|
||||
"showLess": "Приховати додаткові параметри"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Завантажити файл",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Створити обліковий запис",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "Параметри користувача адміністрування",
|
||||
"searchOptions": "Параметри пошуку",
|
||||
"systemAdmin": "Система та адміністрування",
|
||||
"configViewer": "Переглядач конфігурацій",
|
||||
"configViewerShowFull": "Показати повну конфігурацію",
|
||||
"configViewerShowComments": "Показати коментарі",
|
||||
"configViewerLoadConfig": "Завантажити конфігурацію",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "Довірена особа"
|
||||
},
|
||||
"newUserHintSubFolder": "наприклад, '/subfolder' - порожній за замовчуванням",
|
||||
"permissionsHelp": "Дозволи користувача контролюють доступ до того, які дії може виконувати користувач. Доступ адміністратора дає повний контроль над оновленням і зміною будь-яких налаштувань користувача, а також багатьох системних налаштувань.",
|
||||
"configViewer": "Переглядач конфігурацій"
|
||||
"permissionsHelp": "Дозволи користувача контролюють доступ до того, які дії може виконувати користувач. Доступ адміністратора дає повний контроль над оновленням і зміною будь-яких налаштувань користувача, а також багатьох системних налаштувань."
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Налаштування сайту",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "Знайдіть і виберіть піктограму з бібліотеки піктограм матеріалів",
|
||||
"shareLinksUpdatedSuccess": "Посилання на бічну панель обміну успішно оновлено!",
|
||||
"shareHome": "Поділитися Додому",
|
||||
"linkIconPlaceholder": "За бажанням вкажіть назву іконки матеріалу",
|
||||
"dividerNamePlaceholder": "Текст роздільника (залиште порожнім, щоб показати лінію)"
|
||||
"linkIconPlaceholder": "За бажанням вкажіть назву іконки матеріалу"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Посилання скопійоване!"
|
||||
@@ -708,9 +705,7 @@
|
||||
"lines": "Лінії{suffix}",
|
||||
"lastUpdate": "Останнє оновлення{suffix}",
|
||||
"latency": "Затримка{suffix}",
|
||||
"all": "Усе.{suffix}",
|
||||
"expiration": "Закінчення терміну дії{suffix}",
|
||||
"divider": "Перегородка{suffix}"
|
||||
"all": "Усе.{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Правила доступу",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "转到项目",
|
||||
"watchFile": "观看文件",
|
||||
"showMore": "显示高级选项",
|
||||
"showLess": "隐藏高级选项",
|
||||
"goToSource": "转到源位置"
|
||||
"showLess": "隐藏高级选项"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下载文件",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "创建用户",
|
||||
@@ -322,6 +320,7 @@
|
||||
"adminOptions": "管理员用户选项",
|
||||
"searchOptions": "搜索选项",
|
||||
"systemAdmin": "系统与管理",
|
||||
"configViewer": "配置查看器",
|
||||
"configViewerShowFull": "显示完整配置",
|
||||
"configViewerShowComments": "显示评论",
|
||||
"configViewerLoadConfig": "加载配置",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "代理"
|
||||
},
|
||||
"newUserHintSubFolder": "例如\"/子文件夹\"--默认为空",
|
||||
"permissionsHelp": "用户权限控制用户可以执行哪些类型的操作。管理员权限可完全控制更新和更改任何用户的设置以及许多系统设置。",
|
||||
"configViewer": "配置查看器"
|
||||
"permissionsHelp": "用户权限控制用户可以执行哪些类型的操作。管理员权限可完全控制更新和更改任何用户的设置以及许多系统设置。"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "网站设置",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "从材质图标库中搜索并选择图标",
|
||||
"shareLinksUpdatedSuccess": "共享侧边栏链接更新成功!",
|
||||
"shareHome": "共享主页",
|
||||
"linkIconPlaceholder": "可选择指定材料图标名称",
|
||||
"dividerNamePlaceholder": "分隔线文本(留空以显示一行)"
|
||||
"linkIconPlaceholder": "可选择指定材料图标名称"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "链接已复制!"
|
||||
@@ -708,9 +705,7 @@
|
||||
"lines": "线路{suffix}",
|
||||
"lastUpdate": "最新更新{suffix}",
|
||||
"latency": "延迟{suffix}",
|
||||
"all": "全部{suffix}",
|
||||
"expiration": "到期{suffix}",
|
||||
"divider": "分割器{suffix}"
|
||||
"all": "全部{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "访问规则",
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"goToItem": "前往項目",
|
||||
"watchFile": "觀看檔案",
|
||||
"showMore": "顯示進階選項",
|
||||
"showLess": "隱藏進階選項",
|
||||
"goToSource": "前往來源位置"
|
||||
"showLess": "隱藏進階選項"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下載檔案",
|
||||
@@ -115,8 +114,7 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
"cz": "Čeština"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "新建賬戶",
|
||||
@@ -322,6 +320,7 @@
|
||||
"shareDeleted": "分享已刪除!",
|
||||
"adminOptions": "管理員使用者選項",
|
||||
"systemAdmin": "系統與管理",
|
||||
"configViewer": "組態檢視器",
|
||||
"configViewerShowFull": "顯示完整設定",
|
||||
"configViewerShowComments": "顯示評論",
|
||||
"configViewerLoadConfig": "載入組態",
|
||||
@@ -333,8 +332,7 @@
|
||||
"proxy": "代理"
|
||||
},
|
||||
"newUserHintSubFolder": "例如 '/subfolder' - 預設為空白",
|
||||
"permissionsHelp": "使用者權限可控制使用者可執行的動作類型。管理員存取權可完全控制更新和變更任何使用者的設定以及許多系統設定。",
|
||||
"configViewer": "組態檢視器"
|
||||
"permissionsHelp": "使用者權限可控制使用者可執行的動作類型。管理員存取權可完全控制更新和變更任何使用者的設定以及許多系統設定。"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "網站設定",
|
||||
@@ -372,8 +370,7 @@
|
||||
"pickIconDescription": "從 Material Icons 圖示庫搜尋並選取圖示",
|
||||
"shareLinksUpdatedSuccess": "分享側邊欄連結更新成功!",
|
||||
"shareHome": "分享首頁",
|
||||
"linkIconPlaceholder": "可選擇指定材質圖示名稱",
|
||||
"dividerNamePlaceholder": "分割線文字(留空顯示一行)"
|
||||
"linkIconPlaceholder": "可選擇指定材質圖示名稱"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "連結已複製!"
|
||||
@@ -708,9 +705,7 @@
|
||||
"lines": "線路{suffix}",
|
||||
"lastUpdate": "最後更新{suffix}",
|
||||
"latency": "延遲{suffix}",
|
||||
"all": "全部{suffix}",
|
||||
"expiration": "到期日{suffix}",
|
||||
"divider": "分割器{suffix}"
|
||||
"all": "全部{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "存取規則",
|
||||
|
||||
@@ -11,10 +11,10 @@ import { mutations } from "@/store";
|
||||
import { validateLogin } from "@/utils/auth";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
|
||||
const titles = {
|
||||
Login: i18n.global.t("general.login"),
|
||||
Share: i18n.global.t("general.share"),
|
||||
PublicShare: i18n.global.t("general.share"),
|
||||
Files: i18n.global.t("general.files"),
|
||||
Tools: i18n.global.t("general.tool"),
|
||||
Settings: i18n.global.t("general.settings"),
|
||||
@@ -169,9 +169,6 @@ router.beforeResolve(async (to, from, next) => {
|
||||
return next(false);
|
||||
}
|
||||
|
||||
// Clear any popup previews when navigating
|
||||
mutations.setPreviewSource("");
|
||||
|
||||
const title = titles[to.name as keyof typeof titles] || String(to.name || '');
|
||||
document.title = globalVars.name + " - " + title;
|
||||
mutations.setRoute(to);
|
||||
@@ -180,12 +177,11 @@ router.beforeResolve(async (to, from, next) => {
|
||||
to.matched.some((record) => record.meta.requiresAuth) ||
|
||||
to.matched.some((record) => record.meta.optionalAuth)
|
||||
) {
|
||||
const isPublicRoute = to.path.startsWith("/public");
|
||||
if (state?.user?.username) {
|
||||
// do nothing, user is already set
|
||||
} else {
|
||||
try {
|
||||
await validateLogin(isPublicRoute);
|
||||
await validateLogin();
|
||||
} catch (error) {
|
||||
mutations.setCurrentUser(getters.anonymous());
|
||||
}
|
||||
|
||||
@@ -270,9 +270,8 @@ export const getters = {
|
||||
if (urlPath == "/" || urlPath == "") {
|
||||
return "";
|
||||
}
|
||||
// remove hash from path and decode each part
|
||||
let parts = urlPath.split('/').slice(2);
|
||||
urlPath = parts.map(part => decodeURIComponent(part)).join('/')
|
||||
// remove hash from path
|
||||
urlPath = urlPath.split('/').slice(2).join('/')
|
||||
if (subPath != "") {
|
||||
urlPath = url.joinPath(urlPath, removeLeadingSlash(subPath))
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface UserObject {
|
||||
rules?: any[];
|
||||
lockPassword?: boolean;
|
||||
hideDotfiles?: boolean;
|
||||
commands?: string[];
|
||||
sorting?: {
|
||||
by: string;
|
||||
asc: boolean;
|
||||
|
||||
@@ -2,12 +2,9 @@ import { mutations, getters,state } from "@/store";
|
||||
import { getApiPath, getPublicApiPath } from "@/utils/url.js";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
|
||||
export async function validateLogin(isPublicRoute = false) {
|
||||
export async function validateLogin() {
|
||||
// Use direct fetch to avoid automatic logout on 401
|
||||
// Public routes (e.g. /public/share/...) use the public API base path
|
||||
const apiPath = isPublicRoute
|
||||
? getPublicApiPath('users', { id: 'self' })
|
||||
: getApiPath('/api/users', { id: 'self' });
|
||||
let apiPath = getPublicApiPath('users', { id: 'self' });
|
||||
const res = await fetch(apiPath, {
|
||||
credentials: 'same-origin', // Ensure cookies are sent with the request
|
||||
headers: {
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
/**
|
||||
* Strips ASS/SSA override blocks from dialogue text (e.g. {\fad(100,100)\blur3}).
|
||||
* Used only when converting .ass / .ssa to VTT. Other formats keep their styling.
|
||||
*/
|
||||
export function stripAssOverrideTags (text) {
|
||||
if (typeof text !== 'string') return text
|
||||
return text
|
||||
.replace(/\{[^}]*\}/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function convertToVTT (ext, text) {
|
||||
if (ext == '.vtt') {
|
||||
return text
|
||||
@@ -85,7 +73,7 @@ export function convertToVTT (ext, text) {
|
||||
let parts = line.split(',')
|
||||
let startTime = formatAssTime(parts[1].trim())
|
||||
let endTime = formatAssTime(parts[2].trim())
|
||||
let dialogue = stripAssOverrideTags(parts.slice(9).join(',').trim())
|
||||
let dialogue = parts.slice(9).join(',').trim()
|
||||
return `${startTime} --> ${endTime}\n ${dialogue}` // Add leading space before dialogue
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
@@ -79,12 +79,12 @@ export function removePrefix(path, prefix = "") {
|
||||
|
||||
// get path with parameters
|
||||
// Supports array values for repeated parameters (e.g., file=a&file=b&file=c)
|
||||
export function getApiPath(path, params = {}, encode = true) {
|
||||
export function getApiPath(path, params = {}, encode = false) {
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
path = `${globalVars.baseURL}${path}`;
|
||||
|
||||
|
||||
const paramKeys = Object.keys(params);
|
||||
if (paramKeys.length > 0) {
|
||||
if (encode) {
|
||||
@@ -92,7 +92,7 @@ export function getApiPath(path, params = {}, encode = true) {
|
||||
for (const key of paramKeys) {
|
||||
const value = params[key];
|
||||
if (value === undefined) continue;
|
||||
|
||||
|
||||
// Handle array values for repeated parameters
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
@@ -110,7 +110,7 @@ export function getApiPath(path, params = {}, encode = true) {
|
||||
for (const key in params) {
|
||||
const value = params[key];
|
||||
if (value === undefined) continue;
|
||||
|
||||
|
||||
// Handle array values for repeated parameters
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
@@ -125,13 +125,14 @@ export function getApiPath(path, params = {}, encode = true) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// get path with parameters
|
||||
// relative path so it can be used behind proxy
|
||||
export function getPublicApiPath(path, params = {}, encode = true) {
|
||||
return getApiPath(`/public/api/${path}`, params, encode);
|
||||
export function getPublicApiPath(path, params = {}) {
|
||||
return getApiPath(`/public/api/${path}`, params);
|
||||
}
|
||||
|
||||
export function removeTrailingSlash(str) {
|
||||
@@ -210,6 +211,7 @@ export function goToItem(source, path, previousHistoryItem, newTab = false) {
|
||||
if (source == state.sources.current && path == state.req.path) {
|
||||
return;
|
||||
}
|
||||
mutations.setPreviewSource("");
|
||||
let newPath = encodedPath(path);
|
||||
let fullPath = `/files/${encodeURIComponent(source)}${newPath}`;
|
||||
if (newTab) {
|
||||
@@ -228,7 +230,7 @@ export function goToItem(source, path, previousHistoryItem, newTab = false) {
|
||||
router.push({ path: fullPath });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (previousHistoryItem === undefined) {
|
||||
// When undefined will not create browser history
|
||||
router.replace({ path: fullPath });
|
||||
|
||||
@@ -191,9 +191,7 @@ export default {
|
||||
// Parse share route to get subPath
|
||||
let urlPath = getters.routePath('public/share')
|
||||
let parts = urlPath.split("/");
|
||||
// Decode each part since URL paths are encoded
|
||||
let decodedParts = parts.slice(2).map(part => decodeURIComponent(part));
|
||||
shareInfo.subPath = "/" + decodedParts.join("/");
|
||||
shareInfo.subPath = "/" + parts.slice(2).join("/");
|
||||
|
||||
// Set shareInfo in state
|
||||
mutations.setShareInfo(shareInfo);
|
||||
@@ -356,7 +354,8 @@ export default {
|
||||
|
||||
// Display initial data immediately
|
||||
mutations.replaceRequest(file);
|
||||
document.title = globalVars.name + " - " + this.$t("general.share") + " - " + file.name;
|
||||
document.title = `${document.title} - ${file.name}`;
|
||||
|
||||
// Second pass: If directory has metadata available, fetch again with metadata IN THE BACKGROUND
|
||||
if (file.type === "directory" && file.hasMetadata) {
|
||||
this.loadingProgress = 90;
|
||||
@@ -475,7 +474,7 @@ export default {
|
||||
if (state.sources.count > 1) {
|
||||
mutations.setCurrentSource(data.source);
|
||||
}
|
||||
document.title = globalVars.name + " - " + this.$t("general.files") + " - " + res.name;
|
||||
document.title = `${document.title} - ${res.name}`;
|
||||
|
||||
// Display initial data immediately and clear loading spinner
|
||||
mutations.replaceRequest(data);
|
||||
|
||||
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user