Beta/v0.8.9 (#1379)
Co-authored-by: Srinivas A <56465971+srini-abhiram@users.noreply.github.com> Co-authored-by: Kevin B. <kurami32_kbm@proton.me> Co-authored-by: Chanwoong Kim <me@chanwoong.kim>
This commit is contained in:
167 files changed
+6229
-2254
No files matched your search
+1
-2
@@ -20,8 +20,7 @@ frontend/tsconfig.tsbuildinfo
|
||||
/frontend/package-lock.json
|
||||
/backend/vendor
|
||||
/backend/*.cov
|
||||
/backend/test_config.yaml
|
||||
/backend/test-*.yaml
|
||||
/backend/*test_config.yaml
|
||||
/backend/generated.yaml
|
||||
/backend/srv
|
||||
/backend/http/dist
|
||||
|
||||
@@ -2,6 +2,49 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v0.8.9-beta
|
||||
|
||||
> [!CAUTION]
|
||||
> If you have indexing rules -- such as [maxWatchers](https://github.com/gtsteffaniak/filebrowser/blob/39514169f17ed9586d587dd496257588ded6e532/frontend/public/config.generated.yaml#L30), [neverWatchPaths](https://github.com/gtsteffaniak/filebrowser/blob/39514169f17ed9586d587dd496257588ded6e532/frontend/public/config.generated.yaml#L31C9-L31C24), [exclude](https://github.com/gtsteffaniak/filebrowser/blob/39514169f17ed9586d587dd496257588ded6e532/frontend/public/config.generated.yaml#L32C9-L32C16) rules, [include](https://github.com/gtsteffaniak/filebrowser/blob/39514169f17ed9586d587dd496257588ded6e532/frontend/public/config.generated.yaml#L43) rules, see [the migration guide](https://filebrowserquantum.com/en/docs/user-guides/general-configuration/exclusion-rules/) for help.
|
||||
|
||||
**New Features**:
|
||||
- Playback Queue for media -- thanks @Kurami32 #1339
|
||||
- Ctrl+b Shortcut to toggle sidebar, open settings, search #1172
|
||||
- Add a "select all" toggle in the menu #974
|
||||
- Save on exit text editor -- thanks @srini-abhiram #1334 #1241
|
||||
- Disabled indexing for subfolders but allow browsing #783
|
||||
- Added more granular create/upload, delete, and download (includes viewing content) permission #1200 #1199
|
||||
- Upload settings can be adjusted directly in the upload prompt
|
||||
- Added paste confirmation prompt when using copy/paste keyboard shortcuts.
|
||||
- Updated shares
|
||||
- `disableDownload` to disable download and (content viewing).
|
||||
- `allowReplacements` to share permission -- prohibits replacing files on conflict #661
|
||||
- normal shares can create/upload files if given permission
|
||||
|
||||
**Notes**:
|
||||
- Optimized font and icon loading.
|
||||
- the next/previous buttons will follow media queue order if playback mode has a queue.
|
||||
- All new exclude rules -- see [full config example](https://github.com/gtsteffaniak/filebrowser/wiki/Full-Config-Example)
|
||||
- The order of sources will strictly match order in config.yaml. Default landing page is root first source unless otherwise configured in profile settings.
|
||||
- Uploading changes #1371
|
||||
- Chunked uploading can no longer be explicitly disabled, defaults to 5MB chunks
|
||||
- Automatic chunked/non-chunked uploads based on size. If an item exceeds the chunk size, chunked uploading will occur. If its less than chunk size, no chunked upload.
|
||||
- To "disable" chunked upload, you an set your chunk size very large -- 0 chunk size will be reset to 5MB.
|
||||
|
||||
**BugFixes**:
|
||||
- Remember the previous location after closing a preview #1336
|
||||
- fixed issue related to multi-config parsing #1267
|
||||
- Cannot save text files, missing button #1326 #1367
|
||||
- Files are being overwritten and/or content disappears #1312
|
||||
- moving between items using next/previous will reset the req and show a responsive loading spinner -- fixing several state related issues.
|
||||
- access management: child folders accessible stopped showing up #1332
|
||||
- Make source inaccessible if directory does not exist rather than exiting on start #1264
|
||||
- HTTP Proxy environment gets ignored since version v0.8.6-beta #1324
|
||||
- album artwork preview not showing on shares
|
||||
- fixed OIDC logout causing a loop #995 #1361
|
||||
- fixed checksum failure #1372
|
||||
- Copy, paste, cut shortcuts don't work and make the UI unusable #1375
|
||||
|
||||
## v0.8.8-beta
|
||||
|
||||
**New Features**:
|
||||
@@ -42,6 +85,8 @@ All notable changes to this project will be documented in this file. For commit
|
||||
- console errors during login/logout
|
||||
- fix scoped user share creation issue #1309
|
||||
- share close button and navigation issues.
|
||||
- OnlyOffice source not found when opening shared file #1285
|
||||
- user scoped onlyoffice error saving files with Onlyoffice #1068
|
||||
|
||||
## v0.8.7-beta
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
</div>
|
||||
|
||||
> [!WARNING]
|
||||
> There is no stable version -- :construction: planned for 2025. ([Read more](https://github.com/gtsteffaniak/filebrowser/discussions/628))
|
||||
> There is no stable version -- :construction: coming very soon!
|
||||
|
||||
## Pinned
|
||||
|
||||
## Announcements
|
||||
|
||||
[Stable Release & 0.9.0 update](https://github.com/gtsteffaniak/filebrowser/discussions/1293)
|
||||
:loudspeaker: [Stable Release & 0.9.0 Update](https://github.com/gtsteffaniak/filebrowser/discussions/1293)
|
||||
:pushpin: [Read The Official Docs](https://filebrowserquantum.com/) (currently english-only)
|
||||
|
||||
## About
|
||||
|
||||
@@ -65,7 +65,6 @@ Notable features that this fork *does not* have (removed):
|
||||
- :construction: jobs are not supported yet.
|
||||
- ❌ shell commands are completely removed and will not be returned.
|
||||
|
||||
|
||||
FileBrowser Quantum differs significantly from the original version. Many of these changes required a significant overhaul. Creating a fork was a necessary process to make the program better. There have been many growing pains, but a stable release is planned and coming soon.
|
||||
|
||||
## System Requirements
|
||||
@@ -93,31 +92,9 @@ a pop-up menu.
|
||||
<img width="1000" src="https://github.com/user-attachments/assets/aa32b05c-f917-47bb-b07f-857edc5e47f7" title="Search GIF">
|
||||
</p>
|
||||
|
||||
## Install and Configuration
|
||||
## Official Docs
|
||||
|
||||
Check out the [Getting Started Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Getting-Started)
|
||||
|
||||
For help configuring your filebrowser see [Configuration Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration-And-Examples)
|
||||
|
||||
## Command Line Usage
|
||||
|
||||
See the [CLI Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/CLI)
|
||||
|
||||
## API Usage
|
||||
|
||||
See the [API Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/API)
|
||||
|
||||
## Office File Support
|
||||
|
||||
See [Office Support Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Office-Support#adding-open-office-integration-for-docker) on how to enable office file editing and office-related features.
|
||||
|
||||
## Migration from the original filebrowser
|
||||
|
||||
See the [Migration Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Migration)
|
||||
|
||||
## Other Questions
|
||||
|
||||
For more, see the [Q&A Wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Q&A)
|
||||
See the [Official Docs](https://filebrowserquantum.com/). Contributions or welcome and encouraged! See [FilebrowserDocs Github](https://github.com/quantumx-apps/filebrowserDocs).
|
||||
|
||||
## Comparison Chart
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ COPY [ "./_docker/src/noauth/", "./" ]
|
||||
|
||||
WORKDIR /app/frontend
|
||||
COPY [ "./frontend/tests", "./tests" ]
|
||||
COPY [ "./frontend/tests/playwright-files", "./tests/playwright-files2" ]
|
||||
|
||||
WORKDIR /app/backend/
|
||||
COPY [ "./backend/filebrowser", "./"]
|
||||
@@ -13,4 +14,4 @@ RUN mkdir -p ./http/dist
|
||||
COPY [ "./backend/http/embed/", "./http/dist/"]
|
||||
|
||||
# Run tests
|
||||
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
#RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -1,27 +1,9 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
logging:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
name: "playwright + files"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
- path: "."
|
||||
name: "docker"
|
||||
config:
|
||||
disableIndexing: true
|
||||
defaultEnabled: true
|
||||
<<: *server-main
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
styling:
|
||||
customThemes:
|
||||
no-rounded:
|
||||
description: "No rounded corners" # The description of the theme to display in the UI.
|
||||
css: "no-rounded.css" # The css file path and filename to use for the theme.
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
title: "Playwright"
|
||||
<<: *frontend-main
|
||||
|
||||
userDefaults:
|
||||
permissions:
|
||||
download: true
|
||||
create: true
|
||||
@@ -0,0 +1,12 @@
|
||||
frontend-main: &frontend-main
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
styling:
|
||||
customThemes:
|
||||
no-rounded:
|
||||
description: "No rounded corners" # The description of the theme to display in the UI.
|
||||
css: "no-rounded.css" # The css file path and filename to use for the theme.
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
title: "Playwright"
|
||||
@@ -0,0 +1,15 @@
|
||||
server-main: &server-main
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
logging:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
name: "playwright + files"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
- path: "."
|
||||
name: "docker"
|
||||
config:
|
||||
disableIndexing: true
|
||||
defaultEnabled: true
|
||||
@@ -5,11 +5,27 @@ server:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
name: "exclude"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
exclude:
|
||||
folderPaths:
|
||||
- "excluded"
|
||||
conditionals:
|
||||
rules:
|
||||
- folderPath: "excluded"
|
||||
- folderPath: "excludedButVisible"
|
||||
viewable: true
|
||||
- path: "../frontend/tests/playwright-files2"
|
||||
name: "include"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
conditionals:
|
||||
rules:
|
||||
- includeRootItem: "myfolder"
|
||||
- includeRootItem: "subfolderExclusions"
|
||||
- filePath: "/subfolderExclusions/exclusionlist.sh"
|
||||
- folderPath: "/subfolderExclusions/folderItem"
|
||||
viewable: true
|
||||
- fileStartsWith: "startsWith"
|
||||
- folderEndsWith: "Folder"
|
||||
|
||||
auth:
|
||||
methods:
|
||||
@@ -23,4 +39,9 @@ frontend:
|
||||
title: "Playwright"
|
||||
userDefaults:
|
||||
permissions:
|
||||
realtime: true
|
||||
realtime: true
|
||||
|
||||
userDefaults:
|
||||
permissions:
|
||||
download: true
|
||||
create: true
|
||||
@@ -35,4 +35,6 @@ userDefaults:
|
||||
admin: false
|
||||
modify: true
|
||||
share: false
|
||||
api: false
|
||||
api: false
|
||||
download: true
|
||||
create: true
|
||||
@@ -35,4 +35,6 @@ userDefaults:
|
||||
admin: false
|
||||
modify: true
|
||||
share: true
|
||||
api: false
|
||||
api: false
|
||||
download: true
|
||||
create: true
|
||||
@@ -255,12 +255,12 @@ func TestOverrideDirectoryToFile(t *testing.T) {
|
||||
|
||||
// Get the index and set up mock data
|
||||
idx := indexing.GetIndex("test")
|
||||
if idx == nil {
|
||||
if idx == nil { //nolint:staticcheck // t.Fatal terminates execution
|
||||
t.Fatal("Failed to get test index")
|
||||
}
|
||||
|
||||
// Create mock directory structure
|
||||
idx.Directories["/"] = &iteminfo.FileInfo{
|
||||
idx.Directories["/"] = &iteminfo.FileInfo{ //nolint:staticcheck // t.Fatal terminates execution
|
||||
Path: "/",
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Name: "/",
|
||||
@@ -273,7 +273,7 @@ func TestOverrideDirectoryToFile(t *testing.T) {
|
||||
|
||||
// Simulate the directory-to-file override by updating the mock data
|
||||
// Remove the directory from the parent's Folders slice
|
||||
rootInfo := idx.Directories["/"]
|
||||
rootInfo := idx.Directories["/"] //nolint:staticcheck // t.Fatal terminates execution
|
||||
for i, folder := range rootInfo.Folders {
|
||||
if folder.Name == "Test Object" {
|
||||
rootInfo.Folders = append(rootInfo.Folders[:i], rootInfo.Folders[i+1:]...)
|
||||
@@ -330,12 +330,12 @@ func TestOverrideFileToDirectory(t *testing.T) {
|
||||
|
||||
// Get the index and set up mock data
|
||||
idx := indexing.GetIndex("test")
|
||||
if idx == nil {
|
||||
if idx == nil { //nolint:staticcheck // t.Fatal terminates execution
|
||||
t.Fatal("Failed to get test index")
|
||||
}
|
||||
|
||||
// Create mock directory structure with a file
|
||||
idx.Directories["/"] = &iteminfo.FileInfo{
|
||||
idx.Directories["/"] = &iteminfo.FileInfo{ //nolint:staticcheck // t.Fatal terminates execution
|
||||
Path: "/",
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Name: "/",
|
||||
@@ -348,7 +348,7 @@ func TestOverrideFileToDirectory(t *testing.T) {
|
||||
|
||||
// Simulate the file-to-directory override by updating the mock data
|
||||
// Remove the file from the parent's Files slice
|
||||
rootInfo := idx.Directories["/"]
|
||||
rootInfo := idx.Directories["/"] //nolint:staticcheck // t.Fatal terminates execution
|
||||
for i, file := range rootInfo.Files {
|
||||
if file.Name == "Test Object" {
|
||||
rootInfo.Files = append(rootInfo.Files[:i], rootInfo.Files[i+1:]...)
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/access"
|
||||
@@ -43,58 +42,29 @@ func FileInfoFaster(opts utils.FileOptions, access *access.Storage) (*iteminfo.E
|
||||
}
|
||||
opts.IsDir = isDir
|
||||
var info *iteminfo.FileInfo
|
||||
var exists bool
|
||||
var useFsInfo bool
|
||||
|
||||
// Check if indexing is disabled for this source
|
||||
if index.Config.DisableIndexing {
|
||||
useFsInfo = true
|
||||
}
|
||||
// Check if path is viewable (allows filesystem access without indexing)
|
||||
isViewable := index.IsViewable(isDir, opts.Path)
|
||||
|
||||
if !useFsInfo && isDir {
|
||||
// For non-viewable paths, verify they are indexed
|
||||
// Skip this check if indexing is disabled for the entire source
|
||||
if !isViewable && !index.Config.DisableIndexing {
|
||||
err = index.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotIndexed {
|
||||
return response, fmt.Errorf("could not refresh file info: %v", err)
|
||||
}
|
||||
return response, fmt.Errorf("path not accessible: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if useFsInfo {
|
||||
if isDir {
|
||||
info, err = index.GetFsDirInfo(opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
} else {
|
||||
var fileInfo os.FileInfo
|
||||
// Get file info directly from filesystem
|
||||
fileInfo, err = os.Stat(realPath)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("could not stat file: %v", err)
|
||||
}
|
||||
|
||||
info = &iteminfo.FileInfo{
|
||||
Path: opts.Path,
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Name: fileInfo.Name(),
|
||||
Size: fileInfo.Size(),
|
||||
ModTime: fileInfo.ModTime(),
|
||||
},
|
||||
}
|
||||
info.DetectType(realPath, false)
|
||||
if isDir {
|
||||
info, err = index.GetFsDirInfo(opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
} else {
|
||||
info, exists = index.GetReducedMetadata(opts.Path, opts.IsDir)
|
||||
if !exists {
|
||||
err = index.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("could not refresh file info: %v", err)
|
||||
}
|
||||
info, exists = index.GetReducedMetadata(opts.Path, opts.IsDir)
|
||||
if !exists {
|
||||
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
|
||||
}
|
||||
// For files, get info from parent directory to ensure HasPreview is set correctly
|
||||
info, err = index.GetFsDirInfo(opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +244,11 @@ func RefreshIndex(source string, path string, isDir bool, recursive bool) error
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
}
|
||||
|
||||
// Skip indexing for viewable paths (viewable: true means don't index, just allow FS access)
|
||||
if idx.IsViewable(isDir, path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := idx.RefreshFileInfo(utils.FileOptions{Path: path, IsDir: isDir, Recursive: recursive})
|
||||
return err
|
||||
}
|
||||
@@ -307,7 +282,7 @@ func validateMoveDestination(src, dst string, isSrcDir bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MoveResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, realdst string, s *share.Storage) error {
|
||||
func MoveResource(isSrcDir bool, sourceIndex, destIndex, realsrc, realdst string, s *share.Storage) error {
|
||||
// Validate the move operation before executing
|
||||
if err := validateMoveDestination(realsrc, realdst, isSrcDir); err != nil {
|
||||
return err
|
||||
@@ -369,7 +344,7 @@ func MoveResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, rea
|
||||
return nil
|
||||
}
|
||||
|
||||
func CopyResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, realdst string) error {
|
||||
func CopyResource(isSrcDir bool, sourceIndex, destIndex, realsrc, realdst string) error {
|
||||
// Validate the copy operation before executing
|
||||
if err := validateMoveDestination(realsrc, realdst, isSrcDir); err != nil {
|
||||
return err
|
||||
@@ -396,14 +371,12 @@ func CopyResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, rea
|
||||
|
||||
// Refresh destination (parent directory if it's a file)
|
||||
dstRefreshPath := realdst
|
||||
dstRefreshIsDir := isDestDir
|
||||
if !isSrcDir {
|
||||
// If copying a file (regardless of destination), refresh the parent directory
|
||||
dstRefreshPath = filepath.Dir(realdst)
|
||||
dstRefreshIsDir = true
|
||||
}
|
||||
|
||||
go RefreshIndex(destIndex, dstRefreshPath, dstRefreshIsDir, true) //nolint:errcheck
|
||||
go RefreshIndex(destIndex, dstRefreshPath, true, true) //nolint:errcheck
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+26
-1
@@ -107,7 +107,11 @@ func updateUserScopes(user *users.User) bool {
|
||||
|
||||
// func to convert legacy user with perm key to permissions
|
||||
func updatePermissions(user *users.User) bool {
|
||||
updateUser := false
|
||||
if user.Version == 1 {
|
||||
return false
|
||||
}
|
||||
updateUser := true
|
||||
user.Permissions.Download = true
|
||||
// if any keys are true, set the permissions to true
|
||||
if user.Perm.Api {
|
||||
user.Permissions.Api = true
|
||||
@@ -129,6 +133,27 @@ func updatePermissions(user *users.User) bool {
|
||||
user.Perm.Share = false
|
||||
updateUser = true
|
||||
}
|
||||
if user.Perm.Create {
|
||||
user.Permissions.Create = true
|
||||
user.Perm.Create = false
|
||||
updateUser = true
|
||||
}
|
||||
if user.Perm.Create {
|
||||
user.Permissions.Create = true
|
||||
user.Perm.Create = false
|
||||
updateUser = true
|
||||
}
|
||||
if user.Perm.Download {
|
||||
user.Permissions.Download = true
|
||||
user.Perm.Download = false
|
||||
updateUser = true
|
||||
}
|
||||
if user.Permissions.Modify {
|
||||
user.Permissions.Create = true
|
||||
user.Permissions.Delete = true
|
||||
updateUser = true
|
||||
}
|
||||
user.Version = 1
|
||||
if updateUser {
|
||||
createBackup = append(createBackup, true)
|
||||
}
|
||||
|
||||
@@ -23,4 +23,6 @@ var (
|
||||
ErrUnauthorized = errors.New("user unauthorized")
|
||||
ErrNotIndexed = errors.New("directory or item excluded from indexing")
|
||||
ErrWrongLoginMethod = errors.New("user attempted to login with wrong login method")
|
||||
ErrDownloadNotAllowed = errors.New("downloads are not allowed for this share")
|
||||
ErrUploadNotAllowed = errors.New("upload permission not allowed for this share")
|
||||
)
|
||||
@@ -103,7 +103,9 @@ func validateOidcAuth() error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a custom HTTP client with proper User-Agent to avoid being blocked by bot protection
|
||||
transport := &http.Transport{}
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment, // Respect HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables
|
||||
}
|
||||
|
||||
// If disableVerifyTLS is true, disable TLS verification
|
||||
if oidcCfg.DisableVerifyTLS {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -155,16 +156,12 @@ func setupVideoPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
func getRealPath(path string) string {
|
||||
realPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
logger.Fatalf("could not find configured source path: %v", err)
|
||||
}
|
||||
func checkPathExists(realPath string) error {
|
||||
// check path exists
|
||||
if _, err = os.Stat(realPath); os.IsNotExist(err) {
|
||||
logger.Fatalf("configured source path does not exist: %v", realPath)
|
||||
if _, err := os.Stat(realPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("source path %v is currently not available", realPath)
|
||||
}
|
||||
return realPath
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupSources(generate bool) {
|
||||
@@ -175,7 +172,14 @@ func setupSources(generate bool) {
|
||||
if source.Config.Disabled {
|
||||
continue
|
||||
}
|
||||
realPath := getRealPath(source.Path)
|
||||
realPath, err := filepath.Abs(source.Path)
|
||||
if err != nil {
|
||||
logger.Fatalf("error getting real path for source %v: %v", source.Path, err)
|
||||
}
|
||||
err = checkPathExists(realPath)
|
||||
if err != nil {
|
||||
logger.Warningf("error checking path exists: %v", err)
|
||||
}
|
||||
name := filepath.Base(realPath)
|
||||
if name == "\\" {
|
||||
name = strings.Split(realPath, ":")[0]
|
||||
@@ -194,6 +198,7 @@ func setupSources(generate bool) {
|
||||
}
|
||||
}
|
||||
modifyExcludeInclude(source)
|
||||
setConditionals(source)
|
||||
if source.Config.DefaultUserScope == "" {
|
||||
source.Config.DefaultUserScope = "/"
|
||||
}
|
||||
@@ -209,14 +214,12 @@ func setupSources(generate bool) {
|
||||
Config.Server.Sources[0].Config.DefaultEnabled = true
|
||||
}
|
||||
for _, sourcePathOnly := range Config.Server.Sources {
|
||||
var realPath string
|
||||
absPath := sourcePathOnly.Path
|
||||
if generate {
|
||||
// When generating, skip path validation and use the already-set path
|
||||
realPath = sourcePathOnly.Path
|
||||
} else {
|
||||
realPath = getRealPath(sourcePathOnly.Path)
|
||||
absPath = sourcePathOnly.Path
|
||||
}
|
||||
source, ok := Config.Server.SourceMap[realPath]
|
||||
source, ok := Config.Server.SourceMap[absPath]
|
||||
if ok && !slices.Contains(allSourceNames, source.Name) {
|
||||
sourceList = append(sourceList, source)
|
||||
if source.Config.DefaultEnabled {
|
||||
@@ -320,67 +323,65 @@ func setupLogging() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigWithDefaults(configFile string, isGenerate bool) error {
|
||||
Config = setDefaults(isGenerate)
|
||||
// Open and read the YAML file
|
||||
yamlFile, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
func loadConfigWithDefaults(configFile string, generate bool) error {
|
||||
Config = setDefaults(generate)
|
||||
|
||||
// Check if config file exists
|
||||
if _, err := os.Stat(configFile); err != nil {
|
||||
if configFile != "" {
|
||||
logger.Errorf("could not open config file '%v', using default settings.", configFile)
|
||||
}
|
||||
Config.Server.Sources = []*Source{
|
||||
{
|
||||
Path: ".",
|
||||
Config: SourceConfig{
|
||||
DefaultEnabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
loadEnvConfig()
|
||||
return nil
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
|
||||
// Get the directory containing the config file for reference resolution
|
||||
configDir := filepath.Dir(configFile)
|
||||
if configDir == "" {
|
||||
configDir = "."
|
||||
// Try multi-file config first (combine all YAML files in the directory)
|
||||
combinedYAML, err := combineYAMLFiles(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to combine YAML files: %v", err)
|
||||
}
|
||||
|
||||
// Create decoder with ReferenceDirs to support multi-config files
|
||||
// This enables YAML anchors and references to work across multiple files
|
||||
decoder := yaml.NewDecoder(yamlFile,
|
||||
yaml.DisallowUnknownField(),
|
||||
yaml.ReferenceDirs(configDir),
|
||||
)
|
||||
// First pass: Unmarshal into a generic map to resolve all anchors and aliases
|
||||
// This allows YAML anchors defined in auxiliary files to be properly merged
|
||||
var rawConfig map[string]interface{}
|
||||
err = yaml.Unmarshal(combinedYAML, &rawConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing YAML data: %v", err)
|
||||
}
|
||||
|
||||
// Filter to only keep valid top-level Settings struct fields
|
||||
// This removes anchor definitions that are just templates (e.g., "test_server: &test_server")
|
||||
validFields := map[string]bool{
|
||||
"server": true,
|
||||
"auth": true,
|
||||
"integrations": true,
|
||||
"frontend": true,
|
||||
"userDefaults": true,
|
||||
}
|
||||
|
||||
filteredConfig := make(map[string]interface{})
|
||||
for key, value := range rawConfig {
|
||||
if validFields[key] {
|
||||
filteredConfig[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal the filtered config back to YAML
|
||||
filteredYAML, err := yaml.Marshal(filteredConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error re-marshaling filtered YAML: %v", err)
|
||||
}
|
||||
|
||||
// Second pass: Decode with strict validation (disallow unknown fields within valid sections)
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(filteredYAML), yaml.DisallowUnknownField())
|
||||
err = decoder.Decode(&Config)
|
||||
if err != nil {
|
||||
// Fallback to old method if multi-config parsing fails
|
||||
logger.Debugf("Multi-config parsing failed, falling back to single file parsing: %v", err)
|
||||
|
||||
// Reset file position and try old method
|
||||
if _, seekErr := yamlFile.Seek(0, 0); seekErr != nil {
|
||||
return fmt.Errorf("failed to read yaml file: %v", seekErr)
|
||||
}
|
||||
stat, statErr := yamlFile.Stat()
|
||||
if statErr != nil {
|
||||
return statErr
|
||||
}
|
||||
yamlData := make([]byte, stat.Size())
|
||||
_, readErr := yamlFile.Read(yamlData)
|
||||
if readErr != nil && configFile != "config.yaml" {
|
||||
return fmt.Errorf("could not load specified config file: %v", readErr.Error())
|
||||
}
|
||||
if readErr != nil {
|
||||
logger.Warningf("Could not load config file '%v', using default settings: %v", configFile, readErr)
|
||||
}
|
||||
|
||||
// Use simple decoder without ReferenceDirs
|
||||
fallbackErr := yaml.NewDecoder(strings.NewReader(string(yamlData)), yaml.DisallowUnknownField()).Decode(&Config)
|
||||
if fallbackErr != nil {
|
||||
return fmt.Errorf("error unmarshaling YAML data: %v", fallbackErr)
|
||||
}
|
||||
return fmt.Errorf("error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
|
||||
loadEnvConfig()
|
||||
@@ -648,11 +649,15 @@ func GetScopeFromSourcePath(scopes []users.SourceScope, sourcePath string) (stri
|
||||
// assumes backend style scopes
|
||||
func GetSources(u *users.User) []string {
|
||||
sources := []string{}
|
||||
for _, scope := range u.Scopes {
|
||||
source, ok := Config.Server.SourceMap[scope.Name]
|
||||
if ok {
|
||||
sources = append(sources, source.Name)
|
||||
|
||||
// preserves order of sources
|
||||
for _, source := range Config.Server.Sources {
|
||||
_, err := GetScopeFromSourcePath(u.Scopes, source.Path)
|
||||
if err != nil {
|
||||
logger.Warningf("could not get scope for source %v: %v", source.Path, err)
|
||||
continue
|
||||
}
|
||||
sources = append(sources, source.Name)
|
||||
}
|
||||
return sources
|
||||
}
|
||||
@@ -705,27 +710,94 @@ func loadCustomFavicon() {
|
||||
logger.Infof("Successfully validated custom favicon at '%v' (%d bytes, %s)", faviconPath, stat.Size(), ext)
|
||||
}
|
||||
|
||||
func modifyExcludeInclude(config *Source) {
|
||||
normalize := func(s []string, checkExists bool) {
|
||||
for i, v := range s {
|
||||
s[i] = "/" + strings.Trim(v, "/")
|
||||
// check if file/folder exists
|
||||
if checkExists {
|
||||
realPath, err := filepath.Abs(config.Path + s[i])
|
||||
if err != nil {
|
||||
logger.Warningf("could not get absolute path for %v: %v", s[i], err)
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(realPath); os.IsNotExist(err) {
|
||||
logger.Warningf("configured exclude/include path %v does not exist", realPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
// setConditionalsMap builds optimized map structures from conditional rules for O(1) lookups
|
||||
func setConditionals(config *Source) {
|
||||
rules := config.Config.Conditionals.ItemRules
|
||||
|
||||
// Initialize the maps structure (only exact match maps for Names)
|
||||
resolved := &ResolvedConditionalsConfig{
|
||||
FileNames: make(map[string]ConditionalIndexConfig),
|
||||
FolderNames: make(map[string]ConditionalIndexConfig),
|
||||
FilePaths: make(map[string]ConditionalIndexConfig),
|
||||
FolderPaths: make(map[string]ConditionalIndexConfig),
|
||||
FileEndsWith: make([]ConditionalIndexConfig, 0),
|
||||
FolderEndsWith: make([]ConditionalIndexConfig, 0),
|
||||
FileStartsWith: make([]ConditionalIndexConfig, 0),
|
||||
FolderStartsWith: make([]ConditionalIndexConfig, 0),
|
||||
NeverWatchPaths: make(map[string]struct{}),
|
||||
IncludeRootItems: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.FileEndsWith != "" {
|
||||
resolved.FileEndsWith = append(resolved.FileEndsWith, rule)
|
||||
}
|
||||
if rule.FolderEndsWith != "" {
|
||||
resolved.FolderEndsWith = append(resolved.FolderEndsWith, rule)
|
||||
}
|
||||
if rule.FileStartsWith != "" {
|
||||
resolved.FileStartsWith = append(resolved.FileStartsWith, rule)
|
||||
}
|
||||
if rule.FolderStartsWith != "" {
|
||||
resolved.FolderStartsWith = append(resolved.FolderStartsWith, rule)
|
||||
}
|
||||
if rule.FilePath != "" {
|
||||
resolved.FilePaths[rule.FilePath] = rule
|
||||
}
|
||||
if rule.FolderPath != "" {
|
||||
resolved.FolderPaths[rule.FolderPath] = rule
|
||||
}
|
||||
if rule.NeverWatchPath != "" {
|
||||
resolved.NeverWatchPaths[rule.NeverWatchPath] = struct{}{}
|
||||
}
|
||||
if rule.IncludeRootItem != "" {
|
||||
resolved.IncludeRootItems[rule.IncludeRootItem] = struct{}{}
|
||||
}
|
||||
}
|
||||
config.Config.ResolvedConditionals = resolved
|
||||
}
|
||||
|
||||
func modifyExcludeInclude(config *Source) {
|
||||
// Helper to normalize a full path value (FilePaths, FolderPaths)
|
||||
// These always start with "/" and match against full index paths
|
||||
normalizeFullPath := func(value string, checkExists bool) string {
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
normalized := "/" + strings.Trim(value, "/")
|
||||
// check if file/folder exists
|
||||
if checkExists {
|
||||
realPath, err := filepath.Abs(config.Path + normalized)
|
||||
if err != nil {
|
||||
logger.Warningf("could not get absolute path for %v: %v", normalized, err)
|
||||
return normalized
|
||||
}
|
||||
if _, err := os.Stat(realPath); os.IsNotExist(err) {
|
||||
logger.Warningf("configured exclude/include path %v does not exist", realPath)
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// Helper to normalize a name-based value (FileNames, FolderNames, StartsWith, EndsWith)
|
||||
// These match against baseName, so no leading slash
|
||||
normalizeName := func(value string) string {
|
||||
// Just trim slashes - names shouldn't have slashes
|
||||
return strings.Trim(value, "/")
|
||||
}
|
||||
|
||||
// Normalize []ConditionalIndexConfig slices for full paths
|
||||
for i, rule := range config.Config.Conditionals.ItemRules {
|
||||
|
||||
// normalize full paths
|
||||
config.Config.Conditionals.ItemRules[i].FilePath = normalizeFullPath(rule.FilePath, true)
|
||||
config.Config.Conditionals.ItemRules[i].FolderPath = normalizeFullPath(rule.FolderPath, true)
|
||||
config.Config.Conditionals.ItemRules[i].NeverWatchPath = normalizeFullPath(rule.NeverWatchPath, true)
|
||||
config.Config.Conditionals.ItemRules[i].IncludeRootItem = normalizeFullPath(rule.IncludeRootItem, true)
|
||||
|
||||
// normalize names
|
||||
config.Config.Conditionals.ItemRules[i].FileNames = normalizeName(rule.FileNames)
|
||||
config.Config.Conditionals.ItemRules[i].FolderNames = normalizeName(rule.FolderNames)
|
||||
}
|
||||
|
||||
normalize(config.Config.Exclude.FolderPaths, true)
|
||||
normalize(config.Config.Exclude.FilePaths, true)
|
||||
normalize(config.Config.Include.RootFolders, true)
|
||||
normalize(config.Config.Include.RootFiles, true)
|
||||
normalize(config.Config.NeverWatchPaths, true)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package settings
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -42,8 +43,19 @@ func Test_setDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigLoadChanged(t *testing.T) {
|
||||
// Create isolated test directory
|
||||
testDir := t.TempDir()
|
||||
validContent, err := os.ReadFile("./validConfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read validConfig.yaml: %v", err)
|
||||
}
|
||||
configFile := filepath.Join(testDir, "config.yaml")
|
||||
if err = os.WriteFile(configFile, validContent, 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
defaultConfig := setDefaults(true)
|
||||
err := loadConfigWithDefaults("./validConfig.yaml", true)
|
||||
err = loadConfigWithDefaults(configFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("error loading config file: %v", err)
|
||||
}
|
||||
@@ -54,11 +66,22 @@ func TestConfigLoadChanged(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigLoadEnvVars(t *testing.T) {
|
||||
// Create isolated test directory
|
||||
testDir := t.TempDir()
|
||||
validContent, err := os.ReadFile("./validConfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read validConfig.yaml: %v", err)
|
||||
}
|
||||
configFile := filepath.Join(testDir, "config.yaml")
|
||||
if err = os.WriteFile(configFile, validContent, 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
defaultConfig := setDefaults(true)
|
||||
expectedKey := "MYKEY"
|
||||
// mock environment variables
|
||||
os.Setenv("FILEBROWSER_ONLYOFFICE_SECRET", expectedKey)
|
||||
err := loadConfigWithDefaults("./validConfig.yaml", true)
|
||||
err = loadConfigWithDefaults(configFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("error loading config file: %v", err)
|
||||
}
|
||||
@@ -72,8 +95,19 @@ func TestConfigLoadEnvVars(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigLoadSpecificValues(t *testing.T) {
|
||||
// Create isolated test directory
|
||||
testDir := t.TempDir()
|
||||
validContent, err := os.ReadFile("./validConfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read validConfig.yaml: %v", err)
|
||||
}
|
||||
configFile := filepath.Join(testDir, "config.yaml")
|
||||
if err = os.WriteFile(configFile, validContent, 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
defaultConfig := setDefaults(true)
|
||||
err := loadConfigWithDefaults("./validConfig.yaml", true)
|
||||
err = loadConfigWithDefaults(configFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("error loading config file: %v", err)
|
||||
}
|
||||
@@ -93,9 +127,23 @@ func TestConfigLoadSpecificValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInvalidConfig(t *testing.T) {
|
||||
configFile := "./invalidConfig.yaml"
|
||||
err := loadConfigWithDefaults(configFile, true)
|
||||
// Create isolated test directory
|
||||
testDir := t.TempDir()
|
||||
invalidContent, err := os.ReadFile("./invalidConfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read invalidConfig.yaml: %v", err)
|
||||
}
|
||||
configFile := filepath.Join(testDir, "config.yaml")
|
||||
if err = os.WriteFile(configFile, invalidContent, 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
err = loadConfigWithDefaults(configFile, true)
|
||||
// Config loads successfully but validation should catch missing sources
|
||||
if err == nil {
|
||||
t.Fatalf("expected error loading config file %s, got nil", configFile)
|
||||
err = ValidateConfig(Config)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error for config with missing required sources, got nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,6 +705,13 @@ func GenerateYaml() {
|
||||
Config.Server.Sources = []*Source{
|
||||
{
|
||||
Path: ".",
|
||||
Config: SourceConfig{
|
||||
Conditionals: ConditionalFilter{
|
||||
ItemRules: []ConditionalIndexConfig{
|
||||
{}, // Add empty sample rule to show all fields in generated YAML
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -719,6 +726,7 @@ func GenerateYaml() {
|
||||
|
||||
setupSources(true)
|
||||
setupUrls()
|
||||
setupVideoPreview()
|
||||
setupFrontend(true)
|
||||
|
||||
// Restore original paths so the YAML output has the correct paths, not the placeholder
|
||||
|
||||
@@ -29,7 +29,8 @@ simple_auth: &simple_auth
|
||||
enabled: true
|
||||
minLength: 8
|
||||
`
|
||||
anchorsPath := filepath.Join(testDir, "anchors.yaml")
|
||||
// Use pattern-based naming: main config is "config.yaml", so anchors must be "*-config.yaml"
|
||||
anchorsPath := filepath.Join(testDir, "definitions-config.yaml")
|
||||
if err := os.WriteFile(anchorsPath, []byte(anchorsConfig), 0644); err != nil {
|
||||
t.Fatalf("Failed to write anchors config: %v", err)
|
||||
}
|
||||
@@ -61,7 +62,8 @@ userDefaults:
|
||||
}
|
||||
|
||||
// Test loading the multi-config setup
|
||||
err := loadConfigWithDefaults(mainPath, false)
|
||||
// Use generate=true to skip filesystem validation of fake paths
|
||||
err := loadConfigWithDefaults(mainPath, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load multi-config: %v", err)
|
||||
}
|
||||
@@ -128,7 +130,8 @@ frontend:
|
||||
}
|
||||
|
||||
// Test that fallback works for simple configs
|
||||
err := loadConfigWithDefaults(configPath, false)
|
||||
// Use generate=true to skip filesystem validation
|
||||
err := loadConfigWithDefaults(configPath, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load simple config: %v", err)
|
||||
}
|
||||
@@ -163,7 +166,8 @@ base_permissions: &base_permissions
|
||||
admin: false
|
||||
api: false
|
||||
`
|
||||
anchorsPath := filepath.Join(testDir, "anchors.yaml")
|
||||
// Use pattern-based naming: main config is "config.yaml", so anchors must be "*-config.yaml"
|
||||
anchorsPath := filepath.Join(testDir, "definitions-config.yaml")
|
||||
if err := os.WriteFile(anchorsPath, []byte(anchorsConfig), 0644); err != nil {
|
||||
t.Fatalf("Failed to write anchors config: %v", err)
|
||||
}
|
||||
@@ -194,7 +198,8 @@ frontend:
|
||||
}
|
||||
|
||||
// Test loading the nested reference setup
|
||||
err := loadConfigWithDefaults(mainPath, false)
|
||||
// Use generate=true to skip filesystem validation
|
||||
err := loadConfigWithDefaults(mainPath, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load nested config: %v", err)
|
||||
}
|
||||
|
||||
@@ -160,37 +160,59 @@ type Source struct {
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
DenyByDefault bool `json:"denyByDefault,omitempty"` // deny access unless an "allow" access rule was specifically created.
|
||||
Private bool `json:"private"` // designate as source as private -- currently just means no sharing permitted.
|
||||
Disabled bool `json:"disabled,omitempty"` // disable the source, this is useful so you don't need to remove it from the config file
|
||||
IndexingInterval uint32 `json:"indexingIntervalMinutes,omitempty"` // optional manual overide interval in minutes to re-index the source
|
||||
DisableIndexing bool `json:"disableIndexing,omitempty"` // disable the indexing of this source
|
||||
MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported
|
||||
NeverWatchPaths []string `json:"neverWatchPaths"` // paths that get initially once. Useful for folders that rarely change contents (without source path prefix)
|
||||
Exclude ExcludeIndexFilter `json:"exclude"` // exclude files and folders from indexing, if include is not set
|
||||
Include IncludeIndexFilter `json:"include"` // include files and folders from indexing, if exclude is not set
|
||||
DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path
|
||||
DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users?
|
||||
CreateUserDir bool `json:"createUserDir"` // create a user directory for each user
|
||||
IndexAlbumArt bool `json:"indexAlbumArt"` // deprecated: always enabled since 0.8.6
|
||||
DenyByDefault bool `json:"denyByDefault,omitempty"` // deny access unless an "allow" access rule was specifically created.
|
||||
Private bool `json:"private"` // designate as source as private -- currently just means no sharing permitted.
|
||||
Disabled bool `json:"disabled,omitempty"` // disable the source, this is useful so you don't need to remove it from the config file
|
||||
IndexingInterval uint32 `json:"indexingIntervalMinutes,omitempty"` // optional manual overide interval in minutes to re-index the source
|
||||
DisableIndexing bool `json:"disableIndexing,omitempty"` // disable the indexing of this source
|
||||
Conditionals ConditionalFilter `json:"conditionals"` // conditional rules to apply when indexing to include/exclude certain items
|
||||
DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path
|
||||
DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users?
|
||||
CreateUserDir bool `json:"createUserDir"` // create a user directory for each user
|
||||
// hidden but used internally - optimized map lookups for conditional rules
|
||||
ResolvedConditionals *ResolvedConditionalsConfig `json:"-"`
|
||||
}
|
||||
|
||||
type IncludeIndexFilter struct {
|
||||
RootFolders []string `json:"rootFolders"` // list of root folders to include, relative to the source path (eg. "folder1")
|
||||
RootFiles []string `json:"rootFiles"` // list of root files to include, relative to the source path (eg. "file1.txt")
|
||||
type ConditionalFilter struct {
|
||||
Hidden bool `json:"hidden"` // exclude hidden files and folders.
|
||||
ZeroSizeFolders bool `json:"ignoreZeroSizeFolders"` // ignore folders with 0 size
|
||||
ItemRules []ConditionalIndexConfig `json:"rules"` // list of item rules to apply to specific paths
|
||||
}
|
||||
|
||||
type ExcludeIndexFilter struct {
|
||||
Hidden bool `json:"hidden"` // exclude hidden files and folders.
|
||||
ZeroSizeFolders bool `json:"ignoreZeroSizeFolders"` // ignore folders with 0 size
|
||||
FilePaths []string `json:"filePaths"` // list of filepaths Eg. "folder1" or "file1.txt" or "folder1/file1.txt" (without source path prefix)
|
||||
FolderPaths []string `json:"folderPaths"` // (filepath) list of folder names to include/exclude. Eg. "folder1" or "folder1/subfolder" (do not include source path, just the subpaths from the source path)
|
||||
FileNames []string `json:"fileNames"` // (global) list of file names to include/exclude. Eg. "a.jpg"
|
||||
FolderNames []string `json:"folderNames"` // (global) list of folder names to include/exclude. Eg. "@eadir" or ".thumbnails"
|
||||
FileEndsWith []string `json:"fileEndsWith"` // (global) exclude files that end with these suffixes. Eg. ".jpg" or ".txt"
|
||||
FolderEndsWith []string `json:"folderEndsWith"` // (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
|
||||
FileStartsWith []string `json:"fileStartsWith"` // (global) exclude files that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
FolderStartsWith []string `json:"folderStartsWith"` // (global) exclude folders that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
type ConditionalIndexConfig struct {
|
||||
NeverWatchPath string `json:"neverWatchPath"` // index the folder in the first pass to get included in search, but never re-indexed.
|
||||
IncludeRootItem string `json:"includeRootItem"` // include only these items at root folder level
|
||||
FileStartsWith string `json:"fileStartsWith"` // (global) exclude files that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
FolderStartsWith string `json:"folderStartsWith"` // (global) exclude folders that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
FileEndsWith string `json:"fileEndsWith"` // (global) exclude files that end with these suffixes. Eg. ".jpg" or ".txt"
|
||||
FolderEndsWith string `json:"folderEndsWith"` // (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
|
||||
FolderPath string `json:"folderPath"` // (global) exclude folders that match this path. Eg. "/path/to/folder" or "/path/to/folder/subfolder"
|
||||
FilePath string `json:"filePath"` // (global) exclude files that match this path. Eg. "/path/to/file.txt" or "/path/to/file.txt/subfile.txt"
|
||||
FileNames string `json:"fileNames"` // (global) exclude files that match these names. Eg. "file.txt" or "test.csv"
|
||||
FolderNames string `json:"folderNames"` // (global) exclude folders that match these names. Eg. "folder" or "subfolder"
|
||||
Viewable bool `json:"viewable"` // Enable viewing in UI but exclude from indexing
|
||||
}
|
||||
|
||||
// ConditionalMaps provides O(1) lookup performance for conditional rules
|
||||
// Maps are built from ConditionalFilter during initialization
|
||||
type ResolvedConditionalsConfig struct {
|
||||
// Exact match maps - O(1) lookup (only for names, not StartsWith/EndsWith)
|
||||
FileNames map[string]ConditionalIndexConfig // key: file name
|
||||
FolderNames map[string]ConditionalIndexConfig // key: folder name
|
||||
|
||||
// exact match for paths
|
||||
FilePaths map[string]ConditionalIndexConfig // key: file path
|
||||
FolderPaths map[string]ConditionalIndexConfig // key: folder path
|
||||
|
||||
FileEndsWith []ConditionalIndexConfig // list of item rules that have been resolved for specific paths
|
||||
FolderEndsWith []ConditionalIndexConfig // list of item rules that have been resolved for specific paths
|
||||
FileStartsWith []ConditionalIndexConfig // list of item rules that have been resolved for specific paths
|
||||
FolderStartsWith []ConditionalIndexConfig // list of item rules that have been resolved for specific paths
|
||||
|
||||
// NeverWatch paths map - O(1) lookup for all paths with neverWatch: true
|
||||
// This replaces the old NeverWatchPaths slice
|
||||
NeverWatchPaths map[string]struct{} // key: full folder path
|
||||
IncludeRootItems map[string]struct{} // key: inclusive root item
|
||||
}
|
||||
|
||||
type Frontend struct {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// combineYAMLFiles combines multiple YAML files from a directory into a single YAML document
|
||||
// This allows YAML anchors defined in one file to be referenced in another
|
||||
//
|
||||
// Pattern matching: If main config is "config.yaml", only files matching "*-config.yaml" are merged
|
||||
// Examples:
|
||||
// - config.yaml + server-config.yaml + auth-config.yaml
|
||||
// - test.yaml + database-test.yaml + frontend-test.yaml
|
||||
// - myapp.yml + server-myapp.yml
|
||||
func combineYAMLFiles(configFilePath string) ([]byte, error) {
|
||||
// Get absolute path and expand tilde
|
||||
expandedPath := configFilePath
|
||||
if strings.HasPrefix(configFilePath, "~/") {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to expand home directory: %v", err)
|
||||
}
|
||||
expandedPath = filepath.Join(homeDir, configFilePath[2:])
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(expandedPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve config file path: %v", err)
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(absPath)
|
||||
configFileName := filepath.Base(absPath)
|
||||
|
||||
// Extract base name pattern for matching related config files
|
||||
// e.g., "config.yaml" -> match "*-config.yaml"
|
||||
// "test.yml" -> match "*-test.yml"
|
||||
ext := filepath.Ext(configFileName)
|
||||
baseName := strings.TrimSuffix(configFileName, ext)
|
||||
pattern := "-" + baseName + ext
|
||||
|
||||
// Find all YAML files in the directory that match the pattern
|
||||
entries, err := os.ReadDir(configDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config directory: %v", err)
|
||||
}
|
||||
|
||||
var yamlFiles []string
|
||||
var mainConfigContent []byte
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
|
||||
// Skip hidden files (dotfiles like .goreleaser.yaml)
|
||||
if strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
fileExt := strings.ToLower(filepath.Ext(name))
|
||||
|
||||
// Only process .yaml and .yml files
|
||||
if fileExt != ".yaml" && fileExt != ".yml" {
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(configDir, name)
|
||||
|
||||
// Keep track of the main config file to process it last
|
||||
if name == configFileName {
|
||||
content, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read main config file: %v", err)
|
||||
}
|
||||
mainConfigContent = content
|
||||
} else if strings.HasSuffix(name, pattern) {
|
||||
// Only include files that match the pattern (e.g., *-config.yaml)
|
||||
yamlFiles = append(yamlFiles, fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the auxiliary files for consistent ordering
|
||||
sort.Strings(yamlFiles)
|
||||
|
||||
// Combine all YAML files as raw text into a single document
|
||||
// We do NOT use `---` separators because anchors don't work across document boundaries
|
||||
var combined strings.Builder
|
||||
|
||||
// First, add all auxiliary YAML files (these typically contain anchor definitions)
|
||||
for _, file := range yamlFiles {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read YAML file %s: %v", file, err)
|
||||
}
|
||||
combined.Write(content)
|
||||
combined.WriteString("\n") // Add newline between files
|
||||
}
|
||||
|
||||
// Finally, add the main config file (this typically contains the references)
|
||||
if mainConfigContent != nil {
|
||||
combined.Write(mainConfigContent)
|
||||
}
|
||||
|
||||
return []byte(combined.String()), nil
|
||||
}
|
||||
@@ -48,7 +48,6 @@ type FileOptions struct {
|
||||
Path string // realpath
|
||||
Source string
|
||||
IsDir bool
|
||||
Modify bool
|
||||
Expand bool
|
||||
ReadHeader bool
|
||||
Content bool
|
||||
|
||||
@@ -26,12 +26,16 @@ func (s *Storage) CheckChildItemAccess(response *iteminfo.ExtendedFileInfo, inde
|
||||
return errors.ErrAccessDenied
|
||||
}
|
||||
|
||||
// Save original folders and files before filtering
|
||||
originalFolders := response.Folders
|
||||
originalFiles := response.Files
|
||||
|
||||
// Filter and return only the items the user has access to
|
||||
response.Folders = make([]iteminfo.ItemInfo, 0)
|
||||
response.Files = make([]iteminfo.ItemInfo, 0)
|
||||
|
||||
// Check each subfolder for access permissions
|
||||
for _, folder := range response.Folders {
|
||||
for _, folder := range originalFolders {
|
||||
indexPath := parentPath + folder.Name
|
||||
if s.Permitted(index.Path, indexPath, username) {
|
||||
response.Folders = append(response.Folders, folder)
|
||||
@@ -39,7 +43,7 @@ func (s *Storage) CheckChildItemAccess(response *iteminfo.ExtendedFileInfo, inde
|
||||
}
|
||||
|
||||
// Check each subfile for access permissions
|
||||
for _, file := range response.Files {
|
||||
for _, file := range originalFiles {
|
||||
indexPath := parentPath + file.Name
|
||||
if s.Permitted(index.Path, indexPath, username) {
|
||||
response.Files = append(response.Files, file)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
)
|
||||
|
||||
// IsSingleFileShare determines if this share is for a single file (not a directory).
|
||||
@@ -140,3 +143,11 @@ func (l *Link) HasReachedUserLimit(username string) bool {
|
||||
count := l.GetUserDownloadCount(username)
|
||||
return count >= l.DownloadsLimit
|
||||
}
|
||||
|
||||
func (l *Link) GetSourceName() (string, error) {
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[l.Source]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("source not found")
|
||||
}
|
||||
return sourceInfo.Name, nil
|
||||
}
|
||||
@@ -3,9 +3,6 @@ package share
|
||||
import "sync"
|
||||
|
||||
type CommonShare struct {
|
||||
//AllowEdit bool `json:"allowEdit,omitempty"`
|
||||
AllowUpload bool `json:"allowUpload,omitempty"`
|
||||
DisableFileViewer bool `json:"disableFileViewer,omitempty"`
|
||||
DownloadsLimit int `json:"downloadsLimit,omitempty"`
|
||||
ShareTheme string `json:"shareTheme,omitempty"`
|
||||
DisableAnonymous bool `json:"disableAnonymous,omitempty"`
|
||||
@@ -32,6 +29,12 @@ type CommonShare struct {
|
||||
ShareType string `json:"shareType"` // type of share: normal, upload, max
|
||||
PerUserDownloadLimit bool `json:"perUserDownloadLimit,omitempty"`
|
||||
ExtractEmbeddedSubtitles bool `json:"extractEmbeddedSubtitles,omitempty"` // can be io intensive for large files and take 10-30 seconds.
|
||||
AllowDelete bool `json:"allowDelete,omitempty"`
|
||||
AllowCreate bool `json:"allowCreate,omitempty"` // allow creating files
|
||||
AllowModify bool `json:"allowModify,omitempty"` // allow modifying files
|
||||
DisableFileViewer bool `json:"disableFileViewer,omitempty"` // don't allow viewing files
|
||||
DisableDownload bool `json:"disableDownload,omitempty"` // don't allow downloading files
|
||||
AllowReplacements bool `json:"allowReplacements,omitempty"` // allow replacements of files
|
||||
}
|
||||
type CreateBody struct {
|
||||
CommonShare
|
||||
@@ -44,15 +47,17 @@ type CreateBody struct {
|
||||
// Link is the information needed to build a shareable link.
|
||||
type Link struct {
|
||||
CommonShare
|
||||
Mu sync.Mutex `json:"-"`
|
||||
Downloads int `json:"downloads"`
|
||||
UserDownloads map[string]int `json:"-"` // Track downloads per username (not persisted)
|
||||
Hash string `json:"hash" storm:"id,index"`
|
||||
UserID uint `json:"userID"`
|
||||
Expire int64 `json:"expire"`
|
||||
PasswordHash string `json:"password_hash,omitempty"`
|
||||
Downloads int `json:"downloads"`
|
||||
Hash string `json:"hash" storm:"id,index"`
|
||||
UserID uint `json:"userID"`
|
||||
Expire int64 `json:"expire"`
|
||||
PasswordHash string `json:"password_hash,omitempty"`
|
||||
// Token is a random value that will only be set when PasswordHash is set. It is
|
||||
// URL-Safe and is used to download links in password-protected shares via a
|
||||
// query arg.
|
||||
Token string `json:"token,omitempty"`
|
||||
|
||||
Mu sync.Mutex `json:"-"`
|
||||
UserDownloads map[string]int `json:"-"` // Track downloads per username (not persisted)
|
||||
|
||||
}
|
||||
@@ -27,11 +27,14 @@ type AuthToken struct {
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
Api bool `json:"api"`
|
||||
Admin bool `json:"admin"`
|
||||
Modify bool `json:"modify"`
|
||||
Share bool `json:"share"`
|
||||
Realtime bool `json:"realtime"`
|
||||
Api bool `json:"api"` // allow api access
|
||||
Admin bool `json:"admin"` // allow admin access
|
||||
Modify bool `json:"modify"` // allow modifying files
|
||||
Share bool `json:"share"` // allow sharing files
|
||||
Realtime bool `json:"realtime"` // allow realtime updates
|
||||
Delete bool `json:"delete"` // allow deleting files
|
||||
Create bool `json:"create"` // allow creating or uploading files
|
||||
Download bool `json:"download"` // allow downloading files
|
||||
}
|
||||
|
||||
// SortingSettings represents the sorting settings.
|
||||
@@ -69,7 +72,8 @@ type User struct {
|
||||
LoginMethod LoginMethod `json:"loginMethod"`
|
||||
OtpEnabled bool `json:"otpEnabled"` // true if TOTP is enabled, false otherwise
|
||||
// legacy for migration purposes... og filebrowser has perm attribute
|
||||
Perm Permissions `json:"perm,omitzero"`
|
||||
Perm Permissions `json:"perm,omitzero"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type SourceScope struct {
|
||||
|
||||
+15
-23
@@ -4,28 +4,28 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/asdine/storm/v3 v3.2.1
|
||||
github.com/coreos/go-oidc/v3 v3.15.0
|
||||
github.com/coreos/go-oidc/v3 v3.16.0
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||
github.com/gen2brain/go-fitz v1.24.15
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gtsteffaniak/go-cache v1.0.0
|
||||
github.com/gtsteffaniak/go-logger v0.2.3-0.20250926212932-e91a612f674c
|
||||
github.com/kovidgoyal/imaging v1.6.4
|
||||
github.com/gtsteffaniak/go-logger v0.2.3
|
||||
github.com/kovidgoyal/imaging v1.7.0
|
||||
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.42.0
|
||||
golang.org/x/image v0.31.0
|
||||
golang.org/x/mod v0.28.0
|
||||
golang.org/x/oauth2 v0.31.0
|
||||
golang.org/x/sys v0.36.0
|
||||
golang.org/x/time v0.13.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/image v0.32.0
|
||||
golang.org/x/mod v0.29.0
|
||||
golang.org/x/oauth2 v0.32.0
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/time v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -103,19 +103,14 @@ require (
|
||||
github.com/ghostiam/protogetter v0.3.16 // indirect
|
||||
github.com/go-critic/go-critic v0.13.0 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.2 // indirect
|
||||
github.com/go-openapi/spec v0.22.0 // indirect
|
||||
github.com/go-openapi/swag v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
|
||||
@@ -134,7 +129,7 @@ require (
|
||||
github.com/godoc-lint/godoc-lint v0.10.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gohugoio/hugo v0.149.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20250917161122-64cb148137c6 // indirect
|
||||
github.com/golang/geo v0.0.0-20251008113719-ccf458c07da6 // 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
|
||||
@@ -152,7 +147,6 @@ require (
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.5.0 // indirect
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
@@ -162,7 +156,6 @@ require (
|
||||
github.com/jgautheron/goconst v1.8.2 // indirect
|
||||
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
|
||||
github.com/jjti/go-spancheck v0.6.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/julz/importas v0.2.0 // indirect
|
||||
github.com/jupiterrider/ffi v0.5.1 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
|
||||
@@ -238,7 +231,6 @@ require (
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/swaggo/files v1.0.1 // indirect
|
||||
github.com/tdakkota/asciicheck v0.4.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.3 // indirect
|
||||
github.com/tetafro/godot v1.5.4 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
|
||||
@@ -266,10 +258,10 @@ require (
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.38.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
|
||||
|
||||
+34
-139
@@ -51,16 +51,10 @@ github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCg
|
||||
github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
|
||||
github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY=
|
||||
github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
|
||||
github.com/Antonboom/errname v1.1.0 h1:A+ucvdpMwlo/myWrkHEUEBWc/xuXdud23S8tmTb/oAE=
|
||||
github.com/Antonboom/errname v1.1.0/go.mod h1:O1NMrzgUcVBGIfi3xlVuvX8Q/VP/73sseCaAppfjqZw=
|
||||
github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q=
|
||||
github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ=
|
||||
github.com/Antonboom/nilnil v1.1.0 h1:jGxJxjgYS3VUUtOTNk8Z1icwT5ESpLH/426fjmQG+ng=
|
||||
github.com/Antonboom/nilnil v1.1.0/go.mod h1:b7sAlogQjFa1wV8jUW3o4PMzDVFLbTux+xnQdvzdcIE=
|
||||
github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ=
|
||||
github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II=
|
||||
github.com/Antonboom/testifylint v1.6.1 h1:6ZSytkFWatT8mwZlmRCHkWz1gPi+q6UBSbieji2Gj/o=
|
||||
github.com/Antonboom/testifylint v1.6.1/go.mod h1:k+nEkathI2NFjKO6HvwmSrbzUcQ6FAnbZV+ZRrnXPLI=
|
||||
github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
|
||||
github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
|
||||
@@ -71,8 +65,6 @@ github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
||||
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
|
||||
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||
github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
|
||||
github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
@@ -160,8 +152,6 @@ 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.1.1 h1:cQg5KJf9FlctAH4cpL9vLKnziYknoCMCdqXl0wjl72Q=
|
||||
github.com/bombsimon/wsl/v5 v5.1.1/go.mod h1:Gp8lD04z27wm3FANIUPZycXp+8huVsn0oxc+n4qfV9I=
|
||||
github.com/bombsimon/wsl/v5 v5.2.0 h1:PyCCwd3Q7abGs3e34IW4jLYlBS+FbsU6iK+Tb3NnDp4=
|
||||
github.com/bombsimon/wsl/v5 v5.2.0/go.mod h1:Gp8lD04z27wm3FANIUPZycXp+8huVsn0oxc+n4qfV9I=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
@@ -205,8 +195,8 @@ github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyM
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -248,8 +238,6 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -281,8 +269,6 @@ 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.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY=
|
||||
github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
|
||||
github.com/ghostiam/protogetter v0.3.16 h1:UkrisuJBYLnZW6FcYUNBDJOqY3X22RtoYMlCsiNlFFA=
|
||||
github.com/ghostiam/protogetter v0.3.16/go.mod h1:4SRRIv6PcjkIMpUkRUsP4TsUTqO/N3Fmvwivuc/sCHA=
|
||||
github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY=
|
||||
@@ -296,8 +282,8 @@ github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
@@ -306,64 +292,27 @@ 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.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
|
||||
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
|
||||
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
|
||||
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
|
||||
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
|
||||
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
|
||||
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
||||
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
|
||||
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
|
||||
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
|
||||
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
|
||||
github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8=
|
||||
github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo=
|
||||
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
|
||||
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
||||
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
|
||||
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
|
||||
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
|
||||
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
|
||||
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
|
||||
github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU=
|
||||
github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M=
|
||||
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
|
||||
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
|
||||
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
|
||||
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
|
||||
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg=
|
||||
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
|
||||
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
|
||||
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
|
||||
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
|
||||
github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync=
|
||||
github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ=
|
||||
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
|
||||
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
|
||||
github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4=
|
||||
github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE=
|
||||
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
|
||||
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
|
||||
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
|
||||
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
|
||||
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
|
||||
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
|
||||
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
|
||||
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -372,8 +321,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@@ -434,8 +383,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-20250917161122-64cb148137c6 h1:8VSqEdhJCzXKe/SYulyQ7jgHNxP+BoCPmqzWF5STIyw=
|
||||
github.com/golang/geo v0.0.0-20250917161122-64cb148137c6/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
|
||||
github.com/golang/geo v0.0.0-20251008113719-ccf458c07da6 h1:9GWB2BLkXw73MNn6FZZIea1ZHtrdBpffeSz0JFGnj8I=
|
||||
github.com/golang/geo v0.0.0-20251008113719-ccf458c07da6/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -471,15 +420,10 @@ github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4n
|
||||
github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw=
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
|
||||
github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU=
|
||||
github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s=
|
||||
github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
|
||||
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 v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I=
|
||||
github.com/golangci/golangci-lint/v2 v2.4.0 h1:qz6O6vr7kVzXJqyvHjHSz5fA3D+PM8v96QU5gxZCNWM=
|
||||
github.com/golangci/golangci-lint/v2 v2.4.0/go.mod h1:Oq7vuAf6L1iNL34uHDcsIF6Mnc0amOPdsT3/GlpHD+I=
|
||||
github.com/golangci/golangci-lint/v2 v2.5.0 h1:BDRg4ASm4J1y/DSRY6zwJ5tr5Yy8ZqbZ79XrCeFxaQo=
|
||||
github.com/golangci/golangci-lint/v2 v2.5.0/go.mod h1:IJtWJBZkLbx7AVrIUzLd8Oi3ADtwaNpWbR3wthVWHcc=
|
||||
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8=
|
||||
@@ -526,37 +470,24 @@ github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxV
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
|
||||
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
|
||||
github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
|
||||
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
|
||||
github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
|
||||
github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
|
||||
github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=
|
||||
github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=
|
||||
github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk=
|
||||
github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A=
|
||||
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
|
||||
github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=
|
||||
github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
|
||||
github.com/gtsteffaniak/go-cache v1.0.0 h1:CuZw6eI/be6Oi3oR94Jb5LhcZShMCs6+1wvS7WGN8oQ=
|
||||
github.com/gtsteffaniak/go-cache v1.0.0/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
|
||||
github.com/gtsteffaniak/go-logger v0.2.1 h1:NTim7RgpZ0+JAuep+2n1a7oHJbnyaksnWJStdM7qMeU=
|
||||
github.com/gtsteffaniak/go-logger v0.2.1/go.mod h1:U3ZkdAclcxXNMvoQLq+zDkqcTmdqhX3hTf+6AuTo6v0=
|
||||
github.com/gtsteffaniak/go-logger v0.2.2 h1:5bEX4ndHK0CsHZjw76T+j0phKoaEGiDwC8erseF99yU=
|
||||
github.com/gtsteffaniak/go-logger v0.2.2/go.mod h1:U3ZkdAclcxXNMvoQLq+zDkqcTmdqhX3hTf+6AuTo6v0=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3-0.20250926211917-4f1ceadcc169 h1:kW5TWd6IsIB3ypImyoPGWj+jzXaG3muQ1lXdJpj7KCc=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3-0.20250926211917-4f1ceadcc169/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3-0.20250926212455-9b14f7183bd2 h1:gCyi3Whqu0rp8+HG/VEjjBi1vZvp+yBl4c8pHABeV0s=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3-0.20250926212455-9b14f7183bd2/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3-0.20250926212932-e91a612f674c h1:JGf2H8yweS5xYO5kGF0/IDs4WalX3TMhLq4D2gDSQ/g=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3-0.20250926212932-e91a612f674c/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3 h1:8nSC5xSMnMYQI61tsBS1tARpnkv/rOh5lzEXtAG72Nw=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
|
||||
@@ -611,8 +542,8 @@ github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/tt
|
||||
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
|
||||
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
|
||||
github.com/kovidgoyal/imaging v1.7.0 h1:SFWQPJdEuaRY7WhL+MJyoN5IvslgqU+z0iPaoGl3yuM=
|
||||
github.com/kovidgoyal/imaging v1.7.0/go.mod h1:mBprO214rATK/6OaPAUXmHbSMelPSFEmoBAt/IJdmno=
|
||||
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=
|
||||
@@ -621,8 +552,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs=
|
||||
github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I=
|
||||
github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98=
|
||||
github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs=
|
||||
github.com/kunwardeep/paralleltest v1.0.14 h1:wAkMoMeGX/kGfhQBPODT/BL8XhK23ol/nuQ3SwFaUw8=
|
||||
@@ -635,12 +564,8 @@ github.com/ldez/exptostd v0.4.4 h1:58AtQjnLcT/tI5W/1KU7xE/O7zW9RAWB6c/ScQAnfus=
|
||||
github.com/ldez/exptostd v0.4.4/go.mod h1:QfdzPw6oHjFVdNV7ILoPu5sw3OZ3OG1JS0I5JN3J4Js=
|
||||
github.com/ldez/gomoddirectives v0.7.0 h1:EOx8Dd56BZYSez11LVgdj025lKwlP0/E5OLSl9HDwsY=
|
||||
github.com/ldez/gomoddirectives v0.7.0/go.mod h1:wR4v8MN9J8kcwvrkzrx6sC9xe9Cp68gWYCsda5xvyGc=
|
||||
github.com/ldez/grignotin v0.10.0 h1:NQPeh1E/Eza4F0exCeC1WkpnLvgUcQDT8MQ1vOLML0E=
|
||||
github.com/ldez/grignotin v0.10.0/go.mod h1:oR4iCKUP9fwoeO6vCQeD7M5SMxCT6xdVas4vg0h1LaI=
|
||||
github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
|
||||
github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
|
||||
github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk=
|
||||
github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I=
|
||||
github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk=
|
||||
github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI=
|
||||
github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
|
||||
@@ -659,8 +584,6 @@ github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8
|
||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.3.0 h1:VhGqK8gANDvFYDxQkjPbv7/gDJtsGU9k6qj/hC2hgso=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.3.0/go.mod h1:LSo/IQpPfx1dXMcX4ibZCYA7Yy6ayZHIaOGM70+1Wy8=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=
|
||||
github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8=
|
||||
@@ -683,8 +606,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgechev/revive v1.11.0 h1:b/gLLpBE427o+Xmd8G58gSA+KtBwxWinH/A565Awh0w=
|
||||
github.com/mgechev/revive v1.11.0/go.mod h1:tI0oLF/2uj+InHCBLrrqfTKfjtFTBCFFfG05auyzgdw=
|
||||
github.com/mgechev/revive v1.12.0 h1:Q+/kkbbwerrVYPv9d9efaPGmAO/NsxwW/nE6ahpQaCU=
|
||||
github.com/mgechev/revive v1.12.0/go.mod h1:VXsY2LsTigk8XU9BpZauVLjVrhICMOV3k1lpB3CXrp8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
@@ -716,8 +637,6 @@ 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.20.0 h1:OmWLkAFO2HUTYcU6mprnKud1Ey5pVdiVNYGO5HVicx8=
|
||||
github.com/nunnatsa/ginkgolinter v0.20.0/go.mod h1:dCIuFlTPfQerXgGUju3VygfAFPdC5aE1mdacCDKDJcQ=
|
||||
github.com/nunnatsa/ginkgolinter v0.21.0 h1:IYwuX+ajy3G1MezlMLB1BENRtFj16+Evyi4uki1NOOQ=
|
||||
github.com/nunnatsa/ginkgolinter v0.21.0/go.mod h1:QlzY9UP9zaqu58FjYxhp9bnjuwXwG1bfW5rid9ChNMw=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
@@ -732,9 +651,8 @@ github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTH
|
||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY=
|
||||
github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o=
|
||||
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=
|
||||
@@ -817,8 +735,6 @@ 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.22.7 h1:8/9P+oTYI4yIpAzccQKVsg1/90Po+JzGtAhqoHImDeM=
|
||||
github.com/securego/gosec/v2 v2.22.7/go.mod h1:510TFNDMrIPytokyHQAVLvPeDr41Yihn2ak8P+XQfNE=
|
||||
github.com/securego/gosec/v2 v2.22.8 h1:3NMpmfXO8wAVFZPNsd3EscOTa32Jyo6FLLlW53bexMI=
|
||||
github.com/securego/gosec/v2 v2.22.8/go.mod h1:ZAw8K2ikuH9qDlfdV87JmNghnVfKB1XC7+TVzk6Utto=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
@@ -841,16 +757,11 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -878,8 +789,6 @@ github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64
|
||||
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
|
||||
github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=
|
||||
github.com/tdewolff/minify/v2 v2.24.2 h1:vnY3nTulEAbCAAlxTxPPDkzG24rsq31SOzp63yT+7mo=
|
||||
github.com/tdewolff/minify/v2 v2.24.2/go.mod h1:1JrCtoZXaDbqioQZfk3Jdmr0GPJKiU7c1Apmb+7tCeE=
|
||||
github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I=
|
||||
@@ -890,8 +799,6 @@ github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA
|
||||
github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
|
||||
github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4=
|
||||
github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk=
|
||||
github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg=
|
||||
github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
@@ -943,16 +850,12 @@ gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
|
||||
gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
|
||||
go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
|
||||
go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=
|
||||
go-simpler.org/musttag v0.13.1 h1:lw2sJyu7S1X8lc8zWUAdH42y+afdcCnHhWpnkWvd6vU=
|
||||
go-simpler.org/musttag v0.13.1/go.mod h1:8r450ehpMLQgvpb6sg+hV5Ur47eH6olp/3yEanfG97k=
|
||||
go-simpler.org/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.2.0 h1:2NP/XudpPmfBhQKX4rMk+zDYIj//qbt4hfZmSSTcpj8=
|
||||
go.augendre.info/arangolint v0.2.0/go.mod h1:Vx4KSJwu48tkE+8uxuf0cbBnAPgnt8O1KWiT7bljq7w=
|
||||
go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk=
|
||||
go.augendre.info/fatcontext v0.8.0/go.mod h1:oVJfMgwngMsHO+KB2MdgzcO+RvtNdiCEOlWvSFtax/s=
|
||||
go.augendre.info/fatcontext v0.8.1 h1:/T4+cCjpL9g71gJpcFAgVo/K5VFpqlN+NPU7QXxD5+A=
|
||||
go.augendre.info/fatcontext v0.8.1/go.mod h1:r3Qz4ZOzex66wfyyj5VZ1xUcl81vzvHQ6/GWzzlMEwA=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
@@ -982,8 +885,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
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=
|
||||
@@ -998,14 +901,12 @@ golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 h1:Yl4H5w2RV7L/dvSHp2GerziT5K2CORgFINPaMFxWGWw=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621/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.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||
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=
|
||||
@@ -1031,8 +932,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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
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=
|
||||
@@ -1075,16 +976,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.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
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.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.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=
|
||||
@@ -1155,8 +1056,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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.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=
|
||||
@@ -1173,13 +1074,13 @@ 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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -1212,7 +1113,6 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -1222,20 +1122,17 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||
golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
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=
|
||||
@@ -1353,8 +1250,6 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
||||
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
|
||||
mvdan.cc/gofumpt v0.8.0 h1:nZUCeC2ViFaerTcYKstMmfysj6uhQrA2vJe+2vwGU6k=
|
||||
mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg=
|
||||
mvdan.cc/gofumpt v0.9.1 h1:p5YT2NfFWsYyTieYgwcQ8aKV3xRvFH4uuN/zB2gBbMQ=
|
||||
mvdan.cc/gofumpt v0.9.1/go.mod h1:3xYtNemnKiXaTh6R4VtlqDATFwBbdXI8lJvH/4qk7mw=
|
||||
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 h1:WjUu4yQoT5BHT1w8Zu56SP8367OuBV5jvo+4Ulppyf8=
|
||||
|
||||
@@ -49,6 +49,8 @@ func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
Api: strings.Contains(permissionsStr, "api") && d.user.Permissions.Api,
|
||||
Admin: strings.Contains(permissionsStr, "admin") && d.user.Permissions.Admin,
|
||||
Modify: strings.Contains(permissionsStr, "modify") && d.user.Permissions.Modify,
|
||||
Delete: strings.Contains(permissionsStr, "delete") && d.user.Permissions.Delete,
|
||||
Create: strings.Contains(permissionsStr, "create") && d.user.Permissions.Create,
|
||||
Share: strings.Contains(permissionsStr, "share") && d.user.Permissions.Share,
|
||||
Realtime: strings.Contains(permissionsStr, "realtime") && d.user.Permissions.Realtime,
|
||||
}
|
||||
|
||||
@@ -140,6 +140,9 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
|
||||
publicAPI.HandleFunc("GET /onlyoffice/callback", withHashFile(onlyofficeCallbackHandler))
|
||||
publicAPI.HandleFunc("GET /onlyoffice/config", withHashFile(onlyofficeClientConfigGetHandler))
|
||||
publicAPI.HandleFunc("POST /resources", withHashFile(publicUploadHandler))
|
||||
publicAPI.HandleFunc("PUT /resources", withHashFile(publicPutHandler))
|
||||
publicAPI.HandleFunc("DELETE /resources", withHashFile(publicDeleteHandler))
|
||||
publicAPI.HandleFunc("PATCH /resources", withHashFile(publicPatchHandler))
|
||||
|
||||
// Settings routes
|
||||
api.HandleFunc("GET /settings", withAdmin(settingsGetHandler))
|
||||
|
||||
@@ -103,7 +103,6 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
file, err := FileInfoFasterFunc(utils.FileOptions{
|
||||
Path: utils.JoinPathAsUnix(link.Path, path),
|
||||
Source: link.Source,
|
||||
Modify: false,
|
||||
Expand: true,
|
||||
Content: getContent,
|
||||
ExtractEmbeddedSubtitles: settings.Config.Integrations.Media.ExtractEmbeddedSubtitles && link.ExtractEmbeddedSubtitles,
|
||||
@@ -115,7 +114,7 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
file.Token = link.Token
|
||||
file.Source = ""
|
||||
file.Hash = link.Hash
|
||||
if !link.EnableOnlyOffice || !link.DisableFileViewer || reachedDownloadsLimit {
|
||||
if !link.EnableOnlyOffice || link.DisableFileViewer || reachedDownloadsLimit {
|
||||
file.OnlyOfficeId = ""
|
||||
}
|
||||
if getContent && file.Content != "" {
|
||||
|
||||
+40
-32
@@ -54,18 +54,19 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
|
||||
// Extract clean parameters from request
|
||||
source := r.URL.Query().Get("source")
|
||||
path := r.URL.Query().Get("path")
|
||||
providedPath := r.URL.Query().Get("path")
|
||||
hash := r.URL.Query().Get("hash")
|
||||
|
||||
// Validate required parameters
|
||||
if (path == "" || source == "") && d.fileInfo.Hash == "" {
|
||||
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
|
||||
if (providedPath == "" || source == "") && hash == "" {
|
||||
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")
|
||||
}
|
||||
themeMode := utils.Ternary(d.user.DarkMode, "dark", "light")
|
||||
var sourceInfo *settings.Source
|
||||
var ok bool
|
||||
if d.fileInfo.Hash != "" {
|
||||
sourceInfo, ok = settings.Config.Server.SourceMap[source]
|
||||
if hash != "" {
|
||||
sourceInfo, ok = settings.Config.Server.SourceMap[d.share.Source]
|
||||
if !ok {
|
||||
logger.Error("OnlyOffice: source not found")
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found")
|
||||
@@ -77,7 +78,9 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found")
|
||||
}
|
||||
}
|
||||
if d.fileInfo.Hash == "" {
|
||||
source = sourceInfo.Name
|
||||
path := providedPath
|
||||
if hash == "" {
|
||||
// Build file info based on whether this is a share or regular request
|
||||
// Regular user request - need to resolve scope
|
||||
userScope, scopeErr := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
@@ -85,23 +88,22 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
logger.Errorf("OnlyOffice: source %s not available for user %s: %v", source, d.user.Username, scopeErr)
|
||||
return http.StatusForbidden, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
indexPath := utils.JoinPathAsUnix(userScope, path)
|
||||
logger.Debugf("OnlyOffice user request: resolved path=%s", indexPath)
|
||||
path = utils.JoinPathAsUnix(userScope, path)
|
||||
logger.Debugf("OnlyOffice user request: resolved path=%s", path)
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Path: indexPath,
|
||||
Modify: d.user.Permissions.Modify,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
Username: d.user.Username,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
}, store.Access)
|
||||
if err != nil {
|
||||
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, indexPath, err)
|
||||
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, path, err)
|
||||
return errToStatus(err), err
|
||||
}
|
||||
d.fileInfo = *fileInfo
|
||||
} else {
|
||||
source = sourceInfo.Name
|
||||
// path is index path, so we build from share path
|
||||
path = utils.JoinPathAsUnix(d.share.Path, path)
|
||||
path = utils.JoinPathAsUnix(d.share.Path, providedPath)
|
||||
if d.share.EnforceDarkLightMode == "dark" {
|
||||
themeMode = "dark"
|
||||
}
|
||||
@@ -116,19 +118,14 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
canEdit := iteminfo.CanEditOnlyOffice(d.user.Permissions.Modify, fileType)
|
||||
canEditMode := utils.Ternary(canEdit, "edit", "view")
|
||||
if d.fileInfo.Hash != "" {
|
||||
if d.share.EnableOnlyOfficeEditing {
|
||||
canEditMode = "edit"
|
||||
// For shares, check both EnableOnlyOfficeEditing and AllowEdit permissions
|
||||
if d.share.EnableOnlyOfficeEditing && d.share.AllowModify {
|
||||
// Editing also requires authentication (not anonymous)
|
||||
if d.user.Username != "anonymous" {
|
||||
canEditMode = "edit"
|
||||
}
|
||||
}
|
||||
}
|
||||
// For shares, we need to keep track of the original relative path for the callback URL
|
||||
var callbackPath string
|
||||
if d.fileInfo.Hash != "" {
|
||||
// For shares, use the original path parameter (relative to share)
|
||||
callbackPath = r.URL.Query().Get("path")
|
||||
} else {
|
||||
// For regular requests, use the processed path
|
||||
callbackPath = path
|
||||
}
|
||||
|
||||
// Generate document ID for OnlyOffice
|
||||
documentId, err := getOnlyOfficeId(d.fileInfo.RealPath)
|
||||
@@ -138,10 +135,10 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
}
|
||||
|
||||
// Build download URL that OnlyOffice server will use
|
||||
downloadURL := buildOnlyOfficeDownloadURL(source, callbackPath, d.fileInfo.Hash, d.token)
|
||||
downloadURL := buildOnlyOfficeDownloadURL(source, providedPath, d.fileInfo.Hash, d.token)
|
||||
|
||||
// Build callback URL for OnlyOffice to notify us of changes
|
||||
callbackURL := buildOnlyOfficeCallbackURL(source, callbackPath, d.fileInfo.Hash, d.token)
|
||||
callbackURL := buildOnlyOfficeCallbackURL(source, providedPath, d.fileInfo.Hash, d.token)
|
||||
|
||||
// Build OnlyOffice client configuration
|
||||
clientConfig := map[string]interface{}{
|
||||
@@ -261,9 +258,9 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
|
||||
var sourceInfo *settings.Source
|
||||
var ok bool
|
||||
if d.fileInfo.Hash != "" {
|
||||
sourceInfo, ok = settings.Config.Server.SourceMap[source]
|
||||
sourceInfo, ok = settings.Config.Server.SourceMap[d.share.Source]
|
||||
if !ok {
|
||||
logger.Error("OnlyOffice: source not found")
|
||||
logger.Error("OnlyOffice: share source not found")
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found")
|
||||
}
|
||||
} else {
|
||||
@@ -303,6 +300,19 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
|
||||
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
|
||||
data.Status == onlyOfficeStatusForceSaveWhileDocumentStillOpen {
|
||||
|
||||
// Check share permissions first if this is a share request
|
||||
if d.fileInfo.Hash != "" {
|
||||
if !d.share.AllowModify {
|
||||
logger.Warningf("OnlyOffice callback: edit permission not allowed for this share")
|
||||
return http.StatusForbidden, fmt.Errorf("edit permission not allowed for this share")
|
||||
}
|
||||
// Share edit operations also require authentication (not anonymous)
|
||||
if d.user.Username == "anonymous" {
|
||||
logger.Warningf("OnlyOffice callback: edit operations require authentication")
|
||||
return http.StatusForbidden, fmt.Errorf("edit operations require authentication")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify user has modify permissions
|
||||
if !d.user.Permissions.Modify {
|
||||
logger.Warningf("OnlyOffice callback: user %s lacks modify permissions for source=%s, path=%s",
|
||||
@@ -330,7 +340,6 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
|
||||
}
|
||||
resolvedPath = utils.JoinPathAsUnix(userScope, path)
|
||||
}
|
||||
|
||||
// Write the updated document
|
||||
fileOpts := utils.FileOptions{
|
||||
Path: resolvedPath,
|
||||
@@ -341,7 +350,6 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
|
||||
logger.Errorf("OnlyOffice callback: failed to write updated document: %v", writeErr)
|
||||
return http.StatusInternalServerError, writeErr
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Return success response to OnlyOffice server
|
||||
|
||||
+190
-11
@@ -8,8 +8,12 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/preview"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
|
||||
_ "github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
@@ -37,6 +41,11 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
return http.StatusNotImplemented, fmt.Errorf("downloads are disabled for upload shares")
|
||||
}
|
||||
|
||||
// Check DisableDownload permission for normal shares
|
||||
if d.share.DisableDownload {
|
||||
return http.StatusForbidden, fmt.Errorf("downloads are not allowed for this share")
|
||||
}
|
||||
|
||||
// Check global download limit (if not using per-user limits)
|
||||
if !d.share.PerUserDownloadLimit && d.share.DownloadsLimit > 0 && d.share.Downloads >= d.share.DownloadsLimit {
|
||||
return http.StatusForbidden, fmt.Errorf("share downloads limit reached")
|
||||
@@ -103,6 +112,9 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
var status int
|
||||
status, err = rawFilesHandler(w, r, d, fileList)
|
||||
if err != nil {
|
||||
if err == errors.ErrDownloadNotAllowed {
|
||||
return http.StatusForbidden, errors.ErrDownloadNotAllowed
|
||||
}
|
||||
logger.Errorf("public share handler: error processing filelist: '%v' with error %v", f, err)
|
||||
return status, fmt.Errorf("error processing filelist: %v", f)
|
||||
}
|
||||
@@ -150,8 +162,12 @@ func publicGetResourceHandler(w http.ResponseWriter, r *http.Request, d *request
|
||||
// @Failure 501 {object} map[string]string "Uploading disabled for non-upload shares"
|
||||
// @Router /public/api/resources [post]
|
||||
func publicUploadHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
if d.share.ShareType != "upload" {
|
||||
return http.StatusNotImplemented, fmt.Errorf("uploading is disabled for non-upload shares")
|
||||
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")
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(d.share.Path, r.URL.Query().Get("targetPath"))
|
||||
@@ -165,7 +181,7 @@ func publicUploadHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
|
||||
status, err := resourcePostHandler(w, r, d)
|
||||
if err != nil {
|
||||
logger.Errorf("public upload handler: error uploading: '%v' with error %v", d.fileInfo, err)
|
||||
logger.Errorf("public upload handler: error uploading with error %v", err)
|
||||
return http.StatusInternalServerError, fmt.Errorf("upload failure occured on backend")
|
||||
}
|
||||
return status, err
|
||||
@@ -215,15 +231,19 @@ func publicPreviewHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
|
||||
// Restore source name from share for preview generation
|
||||
// The middleware clears file.Source for security, but we need it for index lookups
|
||||
if d.fileInfo.Source == "" && d.share != nil {
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
|
||||
if !ok {
|
||||
// Don't expose internal errors to share users
|
||||
return http.StatusNotFound, fmt.Errorf("resource not available")
|
||||
}
|
||||
d.fileInfo.Source = sourceInfo.Name
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
|
||||
fileInfo, err := FileInfoFasterFunc(utils.FileOptions{
|
||||
Path: utils.JoinPathAsUnix(d.share.Path, d.fileInfo.Path),
|
||||
Source: source,
|
||||
Metadata: true,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("resource not available")
|
||||
}
|
||||
d.fileInfo = *fileInfo
|
||||
status, err := previewHelperFunc(w, r, d)
|
||||
if err != nil {
|
||||
// Obfuscate errors for shares to prevent information leakage
|
||||
@@ -231,3 +251,162 @@ func publicPreviewHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
}
|
||||
return status, err
|
||||
}
|
||||
|
||||
// publicPutHandler handles the PUT request for a public share.
|
||||
// @Summary Update a file in a public share
|
||||
// @Description Updates the content of a file in a public share.
|
||||
// @Tags Public Shares
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param hash query string true "Share hash for authentication"
|
||||
// @Param path query string true "Path to the file to update"
|
||||
// @Param content body string true "New content for the file"
|
||||
// @Success 200 {object} map[string]string "File updated successfully"
|
||||
// @Failure 400 {object} map[string]string "Invalid request or parameters"
|
||||
// @Failure 403 {object} map[string]string "Share unavailable or update not allowed"
|
||||
// @Failure 404 {object} map[string]string "Share not found or file not found"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /public/api/resources [put]
|
||||
func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// update path to be the source path
|
||||
if !d.share.AllowModify {
|
||||
return http.StatusForbidden, fmt.Errorf("create is not allowed for this share")
|
||||
}
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
|
||||
if !d.share.AllowModify {
|
||||
return http.StatusForbidden, fmt.Errorf("edit permission not allowed for this share")
|
||||
}
|
||||
path := r.URL.Query().Get("path")
|
||||
|
||||
// Decode the URL-encoded path
|
||||
path, err = url.QueryUnescape(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
fileOpts := utils.FileOptions{
|
||||
Username: d.user.Username,
|
||||
Path: utils.JoinPathAsUnix(d.share.Path, path),
|
||||
Source: source,
|
||||
Expand: false,
|
||||
}
|
||||
err = files.WriteFile(fileOpts, r.Body)
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
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{
|
||||
Username: d.user.Username,
|
||||
Path: indexPath,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("resource not available")
|
||||
}
|
||||
|
||||
// delete thumbnails
|
||||
preview.DelThumbs(r.Context(), *fileInfo)
|
||||
|
||||
err = files.DeleteFiles(source, fileInfo.RealPath, filepath.Dir(fileInfo.RealPath))
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("an error occured while deleting the resource")
|
||||
}
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
if !d.share.AllowModify {
|
||||
return http.StatusForbidden, fmt.Errorf("edit permission not allowed for this share")
|
||||
}
|
||||
// The middleware clears file.Source for security, but we need it for index lookups
|
||||
source, err := d.share.GetSourceName()
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not available")
|
||||
}
|
||||
action := r.URL.Query().Get("action")
|
||||
encodedFrom := r.URL.Query().Get("from")
|
||||
// Decode the URL-encoded path
|
||||
src, err := url.QueryUnescape(encodedFrom)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
dst := r.URL.Query().Get("destination")
|
||||
dst, err = url.QueryUnescape(dst)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source for share not found")
|
||||
}
|
||||
|
||||
// get full paths for both locations
|
||||
srcFullPath := utils.JoinPathAsUnix(d.share.Path, src)
|
||||
if srcFullPath == "/" {
|
||||
return http.StatusForbidden, fmt.Errorf("an error occured accessing the share")
|
||||
}
|
||||
dstFullPath := utils.JoinPathAsUnix(d.share.Path, dst)
|
||||
if dstFullPath == "/" {
|
||||
return http.StatusForbidden, fmt.Errorf("an error occured accessing the share")
|
||||
}
|
||||
|
||||
// get real source and isDir
|
||||
realSrc, isSrcDir, err := idx.GetRealPath(srcFullPath)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
// get real destination and isDir parent
|
||||
parentRealDest, _, err := idx.GetRealPath(filepath.Dir(dstFullPath))
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
dstFullPath = parentRealDest + "/" + filepath.Base(dst)
|
||||
rename := r.URL.Query().Get("rename") == "true"
|
||||
if rename {
|
||||
dstFullPath = addVersionSuffix(dstFullPath)
|
||||
}
|
||||
// Validate move/rename operation to prevent circular references
|
||||
if action == "rename" || action == "move" {
|
||||
err = validateMoveOperation(realSrc, dstFullPath, isSrcDir)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid move or rename operation")
|
||||
}
|
||||
}
|
||||
err = patchAction(r.Context(), patchActionParams{
|
||||
action: action,
|
||||
srcIndex: source,
|
||||
dstIndex: source,
|
||||
src: realSrc,
|
||||
dst: dstFullPath,
|
||||
d: d,
|
||||
isSrcDir: isSrcDir,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Debugf("Could not run patch action. src=%v dst=%v err=%v", realSrc, dstFullPath, err)
|
||||
return http.StatusInternalServerError, fmt.Errorf("an error occured while processing the request")
|
||||
}
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
+17
-6
@@ -110,12 +110,20 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
if idx == nil {
|
||||
return fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
_, err = files.FileInfoFaster(utils.FileOptions{
|
||||
Username: d.user.Username,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
}, store.Access)
|
||||
if d.share != nil {
|
||||
_, err = files.FileInfoFaster(utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
}, nil)
|
||||
} else {
|
||||
_, err = files.FileInfoFaster(utils.FileOptions{
|
||||
Username: d.user.Username,
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
}, store.Access)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -232,6 +240,9 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
}
|
||||
|
||||
func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext, fileList []string) (int, error) {
|
||||
if !d.user.Permissions.Download && d.share == nil {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to download")
|
||||
}
|
||||
splitFile := strings.Split(fileList[0], "::")
|
||||
if len(splitFile) != 2 {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid file in files request: %v", fileList[0])
|
||||
|
||||
+14
-30
@@ -87,13 +87,9 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
userscope = strings.TrimRight(userscope, "/")
|
||||
scopePath := utils.JoinPathAsUnix(userscope, path)
|
||||
getContent := r.URL.Query().Get("content") == "true"
|
||||
if d.share != nil && d.share.DisableFileViewer {
|
||||
getContent = false
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Username: d.user.Username,
|
||||
Path: scopePath,
|
||||
Modify: d.user.Permissions.Modify,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
Content: getContent,
|
||||
@@ -102,6 +98,9 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if !d.user.Permissions.Download && fileInfo.Content != "" {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to get content, requires download permission")
|
||||
}
|
||||
if userscope != "/" {
|
||||
fileInfo.Path = strings.TrimPrefix(fileInfo.Path, userscope)
|
||||
}
|
||||
@@ -123,6 +122,7 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
} else if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
fileInfo.Checksums = make(map[string]string)
|
||||
fileInfo.Checksums[algo] = checksum
|
||||
}
|
||||
return renderJSON(w, r, fileInfo)
|
||||
@@ -143,6 +143,11 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/resources [delete]
|
||||
func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
|
||||
if !d.user.Permissions.Delete {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to delete")
|
||||
}
|
||||
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
@@ -169,7 +174,6 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
Username: d.user.Username,
|
||||
Path: utils.JoinPathAsUnix(userscope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Permissions.Modify,
|
||||
Expand: false,
|
||||
}, store.Access)
|
||||
if err != nil {
|
||||
@@ -217,17 +221,9 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
logger.Debugf("invalid path encoding: %v", err)
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
shareUpload := false
|
||||
if d.share != nil {
|
||||
if d.share.ShareType == "upload" {
|
||||
shareUpload = true
|
||||
}
|
||||
}
|
||||
if !d.user.Permissions.Modify && !shareUpload {
|
||||
logger.Debugf("user is not allowed to create or modify")
|
||||
if !d.user.Permissions.Create {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
|
||||
}
|
||||
|
||||
// Determine if this is a directory or file based on trailing slash
|
||||
isDir := strings.HasSuffix(path, "/")
|
||||
// Strip trailing slash from userscope to prevent double slashes
|
||||
@@ -242,7 +238,6 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
Username: d.user.Username,
|
||||
Path: utils.JoinPathAsUnix(userscope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Permissions.Modify,
|
||||
Expand: false,
|
||||
}
|
||||
idx := indexing.GetIndex(source)
|
||||
@@ -423,9 +418,7 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid source encoding: %v", err)
|
||||
}
|
||||
if !d.user.Permissions.Modify {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
|
||||
}
|
||||
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
|
||||
// Decode the URL-encoded path
|
||||
@@ -448,7 +441,6 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
Username: d.user.Username,
|
||||
Path: utils.JoinPathAsUnix(userscope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Permissions.Modify,
|
||||
Expand: false,
|
||||
}
|
||||
|
||||
@@ -536,7 +528,7 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
return http.StatusNotFound, fmt.Errorf("source %s not found", dstIndex)
|
||||
}
|
||||
// check target dir exists
|
||||
parentDir, isDstDir, err := idx.GetRealPath(userscopeDst, filepath.Dir(dst))
|
||||
parentDir, _, err := idx.GetRealPath(userscopeDst, filepath.Dir(dst))
|
||||
if err != nil {
|
||||
logger.Debugf("Could not get real path for parent dir: %v %v %v", userscopeDst, filepath.Dir(dst), err)
|
||||
return http.StatusNotFound, err
|
||||
@@ -564,15 +556,10 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to destination path %s", dst)
|
||||
}
|
||||
}
|
||||
overwrite := r.URL.Query().Get("overwrite") == "true"
|
||||
rename := r.URL.Query().Get("rename") == "true"
|
||||
if rename {
|
||||
realDest = addVersionSuffix(realDest)
|
||||
}
|
||||
// Permission for overwriting the file
|
||||
if overwrite && !d.user.Permissions.Modify {
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user does not have permission to overwrite file")
|
||||
}
|
||||
|
||||
// Validate move/rename operation to prevent circular references
|
||||
if action == "rename" || action == "move" {
|
||||
@@ -589,7 +576,6 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
dst: realDest,
|
||||
d: d,
|
||||
isSrcDir: isSrcDir,
|
||||
isDstDir: isDstDir,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Debugf("Could not run patch action. src=%v dst=%v err=%v", realSrc, realDest, err)
|
||||
@@ -621,13 +607,12 @@ type patchActionParams struct {
|
||||
dst string
|
||||
d *requestContext
|
||||
isSrcDir bool
|
||||
isDstDir bool
|
||||
}
|
||||
|
||||
func patchAction(ctx context.Context, params patchActionParams) error {
|
||||
switch params.action {
|
||||
case "copy":
|
||||
err := files.CopyResource(params.isSrcDir, params.isDstDir, params.srcIndex, params.dstIndex, params.src, params.dst)
|
||||
err := files.CopyResource(params.isSrcDir, params.srcIndex, params.dstIndex, params.src, params.dst)
|
||||
return err
|
||||
case "rename", "move":
|
||||
idx := indexing.GetIndex(params.srcIndex)
|
||||
@@ -640,7 +625,6 @@ func patchAction(ctx context.Context, params patchActionParams) error {
|
||||
Path: srcPath,
|
||||
Source: params.srcIndex,
|
||||
IsDir: params.isSrcDir,
|
||||
Modify: params.d.user.Permissions.Modify,
|
||||
}, store.Access)
|
||||
|
||||
if err != nil {
|
||||
@@ -649,7 +633,7 @@ func patchAction(ctx context.Context, params patchActionParams) error {
|
||||
|
||||
// delete thumbnails
|
||||
preview.DelThumbs(ctx, *fileInfo)
|
||||
return files.MoveResource(params.isSrcDir, params.isDstDir, params.srcIndex, params.dstIndex, params.src, params.dst, store.Share)
|
||||
return files.MoveResource(params.isSrcDir, params.srcIndex, params.dstIndex, params.src, params.dst, store.Share)
|
||||
default:
|
||||
return fmt.Errorf("unsupported action %s: %w", params.action, errors.ErrInvalidRequestParams)
|
||||
}
|
||||
|
||||
@@ -224,6 +224,9 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
body.Path = s.Path
|
||||
body.Source = s.Source
|
||||
s.CommonShare = body.CommonShare
|
||||
if s.ShareType == "upload" && !body.AllowCreate {
|
||||
s.AllowCreate = true
|
||||
}
|
||||
|
||||
// Reset download counts if limit settings changed
|
||||
if shouldResetCounts {
|
||||
@@ -281,6 +284,9 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
return http.StatusForbidden, fmt.Errorf("path not found: %s", body.Path)
|
||||
}
|
||||
}
|
||||
if body.ShareType == "upload" && !body.AllowCreate {
|
||||
body.AllowCreate = true
|
||||
}
|
||||
body.Source = source.Path // backend source is path
|
||||
s = &share.Link{
|
||||
Expire: expire,
|
||||
|
||||
@@ -106,6 +106,18 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
shareProps["shareType"] = utils.Ternary(d.share.ShareType == "", "normal", d.share.ShareType)
|
||||
shareProps["perUserDownloadLimit"] = d.share.PerUserDownloadLimit
|
||||
shareProps["extractEmbeddedSubtitles"] = d.share.ExtractEmbeddedSubtitles
|
||||
shareProps["disableDownload"] = d.share.DisableDownload
|
||||
shareProps["allowCreate"] = d.share.AllowCreate
|
||||
shareProps["allowModify"] = d.share.AllowModify
|
||||
shareProps["allowDelete"] = d.share.AllowDelete
|
||||
shareProps["allowReplacements"] = d.share.AllowReplacements
|
||||
shareProps["downloadsLimit"] = d.share.DownloadsLimit
|
||||
shareProps["shareTheme"] = d.share.ShareTheme
|
||||
shareProps["disableAnonymous"] = d.share.DisableAnonymous
|
||||
shareProps["maxBandwidth"] = d.share.MaxBandwidth
|
||||
shareProps["keepAfterExpiration"] = d.share.KeepAfterExpiration
|
||||
shareProps["allowedUsernames"] = d.share.AllowedUsernames
|
||||
shareProps["hideNavButtons"] = d.share.HideNavButtons
|
||||
|
||||
// Additional computed properties from extended.go
|
||||
shareProps["hasPassword"] = d.share.HasPassword()
|
||||
|
||||
@@ -14,6 +14,8 @@ func errToStatus(err error) int {
|
||||
return http.StatusOK
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden
|
||||
case errors.Is(err, libErrors.ErrAccessDenied):
|
||||
return http.StatusForbidden
|
||||
case os.IsNotExist(err), err == libErrors.ErrNotExist:
|
||||
return http.StatusNotFound
|
||||
case os.IsExist(err), err == libErrors.ErrExist:
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
package indexing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
)
|
||||
|
||||
// setupConditionalTestIndex creates a test index with conditional rules configured
|
||||
func setupConditionalTestIndex(conditionals settings.ConditionalFilter) *Index {
|
||||
source := &settings.Source{
|
||||
Name: "test",
|
||||
Path: "/test/path",
|
||||
Config: settings.SourceConfig{
|
||||
DisableIndexing: false,
|
||||
Conditionals: conditionals,
|
||||
},
|
||||
}
|
||||
|
||||
// Build the conditional maps (this is normally done during config loading)
|
||||
source.Config.ResolvedConditionals = &settings.ResolvedConditionalsConfig{
|
||||
FileNames: make(map[string]settings.ConditionalIndexConfig),
|
||||
FolderNames: make(map[string]settings.ConditionalIndexConfig),
|
||||
FilePaths: make(map[string]settings.ConditionalIndexConfig),
|
||||
FolderPaths: make(map[string]settings.ConditionalIndexConfig),
|
||||
NeverWatchPaths: make(map[string]struct{}),
|
||||
IncludeRootItems: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
// Build maps from ItemRules - match the real setConditionals implementation
|
||||
resolved := source.Config.ResolvedConditionals
|
||||
for _, rule := range conditionals.ItemRules {
|
||||
// Note: FileNames and FolderNames are NOT populated from rules in the real implementation
|
||||
// They remain empty maps (unused in current implementation)
|
||||
|
||||
// Handle exact path matches
|
||||
if rule.FilePath != "" {
|
||||
resolved.FilePaths[rule.FilePath] = rule
|
||||
}
|
||||
if rule.FolderPath != "" {
|
||||
resolved.FolderPaths[rule.FolderPath] = rule
|
||||
}
|
||||
|
||||
// Handle StartsWith/EndsWith (stored in slices)
|
||||
if rule.FileEndsWith != "" {
|
||||
resolved.FileEndsWith = append(resolved.FileEndsWith, rule)
|
||||
}
|
||||
if rule.FolderEndsWith != "" {
|
||||
resolved.FolderEndsWith = append(resolved.FolderEndsWith, rule)
|
||||
}
|
||||
if rule.FileStartsWith != "" {
|
||||
resolved.FileStartsWith = append(resolved.FileStartsWith, rule)
|
||||
}
|
||||
if rule.FolderStartsWith != "" {
|
||||
resolved.FolderStartsWith = append(resolved.FolderStartsWith, rule)
|
||||
}
|
||||
|
||||
// Handle NeverWatchPath
|
||||
if rule.NeverWatchPath != "" {
|
||||
resolved.NeverWatchPaths[rule.NeverWatchPath] = struct{}{}
|
||||
}
|
||||
|
||||
// Handle IncludeRootItem
|
||||
if rule.IncludeRootItem != "" {
|
||||
resolved.IncludeRootItems[rule.IncludeRootItem] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return &Index{
|
||||
Source: *source,
|
||||
mock: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_FolderStartsWith(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ruleValue string
|
||||
fullPath string
|
||||
baseName string
|
||||
shouldSkip bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Skip folder starting with 'graham'",
|
||||
ruleValue: "graham", // After normalization (no leading slash)
|
||||
fullPath: "/graham/", // Full index path
|
||||
baseName: "graham", // Base name
|
||||
shouldSkip: true,
|
||||
description: "Folder 'graham' should be skipped",
|
||||
},
|
||||
{
|
||||
name: "Skip folder starting with 'temp'",
|
||||
ruleValue: "temp",
|
||||
fullPath: "/temp/",
|
||||
baseName: "temp",
|
||||
shouldSkip: true,
|
||||
description: "Folder 'temp' should be skipped",
|
||||
},
|
||||
{
|
||||
name: "Skip folder starting with 'tmp-'",
|
||||
ruleValue: "tmp-",
|
||||
fullPath: "/tmp-backup/",
|
||||
baseName: "tmp-backup",
|
||||
shouldSkip: true,
|
||||
description: "Folder 'tmp-backup' starts with 'tmp-' and should be skipped",
|
||||
},
|
||||
{
|
||||
name: "Don't skip folder not starting with rule",
|
||||
ruleValue: "graham",
|
||||
fullPath: "/other/",
|
||||
baseName: "other",
|
||||
shouldSkip: false,
|
||||
description: "Folder 'other' doesn't start with 'graham'",
|
||||
},
|
||||
{
|
||||
name: "Don't skip folder that contains but doesn't start",
|
||||
ruleValue: "graham",
|
||||
fullPath: "/mygraham/",
|
||||
baseName: "mygraham",
|
||||
shouldSkip: false,
|
||||
description: "Folder 'mygraham' contains but doesn't start with 'graham'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{FolderStartsWith: tt.ruleValue},
|
||||
},
|
||||
})
|
||||
|
||||
result := idx.shouldSkip(true, false, tt.fullPath, tt.baseName, actionConfig{})
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("%s: expected shouldSkip=%v, got %v (fullPath=%s, baseName=%s, rule=%s)",
|
||||
tt.description, tt.shouldSkip, result, tt.fullPath, tt.baseName, tt.ruleValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_FolderNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ruleValue string
|
||||
baseName string
|
||||
shouldSkip bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Skip exact folder name match",
|
||||
ruleValue: "node_modules",
|
||||
baseName: "node_modules",
|
||||
shouldSkip: true,
|
||||
description: "Exact match should be skipped",
|
||||
},
|
||||
{
|
||||
name: "Skip folder name '@eaDir'",
|
||||
ruleValue: "@eaDir",
|
||||
baseName: "@eaDir",
|
||||
shouldSkip: true,
|
||||
description: "Synology thumbnail folder should be skipped",
|
||||
},
|
||||
{
|
||||
name: "Don't skip partial match",
|
||||
ruleValue: "node_modules",
|
||||
baseName: "node_modules_backup",
|
||||
shouldSkip: false,
|
||||
description: "Partial match should not be skipped",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{FolderNames: tt.ruleValue},
|
||||
},
|
||||
})
|
||||
|
||||
// Manually populate FolderNames map since setConditionals doesn't do it
|
||||
idx.Config.ResolvedConditionals.FolderNames[tt.ruleValue] = settings.ConditionalIndexConfig{FolderNames: tt.ruleValue}
|
||||
|
||||
result := idx.shouldSkip(true, false, "/"+tt.baseName+"/", tt.baseName, actionConfig{})
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("%s: expected shouldSkip=%v, got %v (baseName=%s, rule=%s)",
|
||||
tt.description, tt.shouldSkip, result, tt.baseName, tt.ruleValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_FolderPaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ruleValue string
|
||||
fullPath string
|
||||
baseName string
|
||||
shouldSkip bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Skip exact folder path",
|
||||
ruleValue: "/graham/", // Exact path with trailing slash
|
||||
fullPath: "/graham/", // Full index path
|
||||
baseName: "graham", // Base name
|
||||
shouldSkip: true,
|
||||
description: "Exact path match should be skipped (O(1) lookup)",
|
||||
},
|
||||
{
|
||||
name: "Skip subfolder with prefix matching",
|
||||
ruleValue: "/graham/",
|
||||
fullPath: "/graham/subfolder/",
|
||||
baseName: "subfolder",
|
||||
shouldSkip: true,
|
||||
description: "FolderPaths now does prefix matching for child folders",
|
||||
},
|
||||
{
|
||||
name: "Don't skip unrelated path",
|
||||
ruleValue: "/graham/",
|
||||
fullPath: "/other/",
|
||||
baseName: "other",
|
||||
shouldSkip: false,
|
||||
description: "Unrelated path should not be skipped",
|
||||
},
|
||||
{
|
||||
name: "Skip nested path and its children",
|
||||
ruleValue: "/projects/old/",
|
||||
fullPath: "/projects/old/backup/",
|
||||
baseName: "backup",
|
||||
shouldSkip: true,
|
||||
description: "Nested path under rule should be skipped",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{FolderPath: tt.ruleValue},
|
||||
},
|
||||
})
|
||||
|
||||
result := idx.shouldSkip(true, false, tt.fullPath, tt.baseName, actionConfig{})
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("%s: expected shouldSkip=%v, got %v (fullPath=%s, baseName=%s, rule=%s)",
|
||||
tt.description, tt.shouldSkip, result, tt.fullPath, tt.baseName, tt.ruleValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_FileStartsWith(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ruleValue string
|
||||
baseName string
|
||||
shouldSkip bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Skip file starting with 'Docker'",
|
||||
ruleValue: "Docker", // After normalization (no leading slash)
|
||||
baseName: "Docker.dmg",
|
||||
shouldSkip: true,
|
||||
description: "File 'Docker.dmg' should be skipped",
|
||||
},
|
||||
{
|
||||
name: "Skip file starting with '.'",
|
||||
ruleValue: ".",
|
||||
baseName: ".DS_Store",
|
||||
shouldSkip: true,
|
||||
description: "Hidden file should be skipped",
|
||||
},
|
||||
{
|
||||
name: "Don't skip file not starting with rule",
|
||||
ruleValue: "tmp",
|
||||
baseName: "document.txt",
|
||||
shouldSkip: false,
|
||||
description: "File not starting with 'tmp' should not be skipped",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{FileStartsWith: tt.ruleValue},
|
||||
},
|
||||
})
|
||||
|
||||
result := idx.shouldSkip(false, false, "/"+tt.baseName, tt.baseName, actionConfig{})
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("%s: expected shouldSkip=%v, got %v (baseName=%s, rule=%s)",
|
||||
tt.description, tt.shouldSkip, result, tt.baseName, tt.ruleValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_FileEndsWith(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ruleValue string
|
||||
baseName string
|
||||
shouldSkip bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Skip file ending with '.tmp'",
|
||||
ruleValue: ".tmp",
|
||||
baseName: "document.tmp",
|
||||
shouldSkip: true,
|
||||
description: "Temporary file should be skipped",
|
||||
},
|
||||
{
|
||||
name: "Skip file ending with '~'",
|
||||
ruleValue: "~",
|
||||
baseName: "document.txt~",
|
||||
shouldSkip: true,
|
||||
description: "Backup file should be skipped",
|
||||
},
|
||||
{
|
||||
name: "Don't skip file with different extension",
|
||||
ruleValue: ".tmp",
|
||||
baseName: "document.txt",
|
||||
shouldSkip: false,
|
||||
description: "Regular file should not be skipped",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{FileEndsWith: tt.ruleValue},
|
||||
},
|
||||
})
|
||||
|
||||
result := idx.shouldSkip(false, false, "/"+tt.baseName, tt.baseName, actionConfig{})
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("%s: expected shouldSkip=%v, got %v (baseName=%s, rule=%s)",
|
||||
tt.description, tt.shouldSkip, result, tt.baseName, tt.ruleValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_FilePaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ruleValue string
|
||||
fullPath string
|
||||
baseName string
|
||||
shouldSkip bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Skip exact file path",
|
||||
ruleValue: "/config.txt",
|
||||
fullPath: "/config.txt",
|
||||
baseName: "config.txt",
|
||||
shouldSkip: true,
|
||||
description: "Exact file path match should be skipped (O(1) lookup)",
|
||||
},
|
||||
{
|
||||
name: "Skip file in excluded folder with prefix matching",
|
||||
ruleValue: "/logs",
|
||||
fullPath: "/logs/app.log",
|
||||
baseName: "app.log",
|
||||
shouldSkip: true,
|
||||
description: "FilePaths now does prefix matching for files in excluded folders",
|
||||
},
|
||||
{
|
||||
name: "Don't skip file in different folder",
|
||||
ruleValue: "/logs",
|
||||
fullPath: "/data/file.txt",
|
||||
baseName: "file.txt",
|
||||
shouldSkip: false,
|
||||
description: "File in different folder should not be skipped",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{FilePath: tt.ruleValue},
|
||||
},
|
||||
})
|
||||
|
||||
result := idx.shouldSkip(false, false, tt.fullPath, tt.baseName, actionConfig{})
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("%s: expected shouldSkip=%v, got %v (fullPath=%s, rule=%s)",
|
||||
tt.description, tt.shouldSkip, result, tt.fullPath, tt.ruleValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_FileInExcludedFolderPath(t *testing.T) {
|
||||
// Test that files in excluded folder paths are also skipped
|
||||
// Uses FilePaths rule which applies to both the folder and its files via prefix matching
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{FilePath: "/graham/"},
|
||||
},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
fullPath string
|
||||
baseName string
|
||||
shouldSkip bool
|
||||
reason string
|
||||
}{
|
||||
{"/graham/file.txt", "file.txt", true, "File in excluded folder should be skipped"},
|
||||
{"/graham/subfolder/file.txt", "file.txt", true, "File in subfolder of excluded folder should be skipped"},
|
||||
{"/other/file.txt", "file.txt", false, "File in different folder should not be skipped"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := idx.shouldSkip(false, false, tt.fullPath, tt.baseName, actionConfig{})
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("File %s (%s): expected shouldSkip=%v, got %v",
|
||||
tt.fullPath, tt.reason, tt.shouldSkip, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_MultipleRules(t *testing.T) {
|
||||
// Test that multiple rules work together
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{FolderStartsWith: "temp"},
|
||||
{FolderStartsWith: "tmp"},
|
||||
{FolderNames: "node_modules"},
|
||||
{FolderNames: "@eaDir"},
|
||||
{FileStartsWith: "."},
|
||||
{FileEndsWith: ".tmp"},
|
||||
{FileEndsWith: "~"},
|
||||
},
|
||||
})
|
||||
|
||||
// Manually populate FolderNames map
|
||||
idx.Config.ResolvedConditionals.FolderNames["node_modules"] = settings.ConditionalIndexConfig{FolderNames: "node_modules"}
|
||||
idx.Config.ResolvedConditionals.FolderNames["@eaDir"] = settings.ConditionalIndexConfig{FolderNames: "@eaDir"}
|
||||
|
||||
tests := []struct {
|
||||
isDir bool
|
||||
fullPath string
|
||||
baseName string
|
||||
shouldSkip bool
|
||||
reason string
|
||||
}{
|
||||
// Folders
|
||||
{true, "/temp/", "temp", true, "starts with temp"},
|
||||
{true, "/tmp-backup/", "tmp-backup", true, "starts with tmp"},
|
||||
{true, "/node_modules/", "node_modules", true, "exact name match"},
|
||||
{true, "/@eaDir/", "@eaDir", true, "exact name match"},
|
||||
{true, "/regular/", "regular", false, "no rule matches"},
|
||||
|
||||
// Files
|
||||
{false, "/.DS_Store", ".DS_Store", true, "starts with ."},
|
||||
{false, "/file.tmp", "file.tmp", true, "ends with .tmp"},
|
||||
{false, "/file.txt~", "file.txt~", true, "ends with ~"},
|
||||
{false, "/document.txt", "document.txt", false, "no rule matches"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := idx.shouldSkip(tt.isDir, false, tt.fullPath, tt.baseName, actionConfig{})
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("%s (%s): expected shouldSkip=%v, got %v",
|
||||
tt.baseName, tt.reason, tt.shouldSkip, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_ViewableStillSkips(t *testing.T) {
|
||||
// Test that Viewable:true doesn't affect shouldSkip behavior
|
||||
// The Viewable field is checked separately in IsViewable(), not in shouldSkip()
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{FolderStartsWith: "important", Viewable: true}, // Viewable but still skipped from indexing
|
||||
},
|
||||
})
|
||||
|
||||
// shouldSkip still returns true even with Viewable:true (it's checked separately)
|
||||
result := idx.shouldSkip(true, false, "/important/", "important", actionConfig{})
|
||||
if result != true {
|
||||
t.Errorf("Folder should be skipped (Viewable is checked separately), got shouldSkip=%v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_HiddenFiles(t *testing.T) {
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
Hidden: true, // Skip hidden files
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
isHidden bool
|
||||
shouldSkip bool
|
||||
}{
|
||||
{true, true},
|
||||
{false, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := idx.shouldSkip(false, tt.isHidden, "/file.txt", "file.txt", actionConfig{})
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("isHidden=%v: expected shouldSkip=%v, got %v",
|
||||
tt.isHidden, tt.shouldSkip, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkip_NeverWatch(t *testing.T) {
|
||||
// Test NeverWatch functionality - folders should be skipped during routine scans
|
||||
idx := setupConditionalTestIndex(settings.ConditionalFilter{
|
||||
ItemRules: []settings.ConditionalIndexConfig{
|
||||
{NeverWatchPath: "/cache/"}, // Must match the fullPath exactly
|
||||
{NeverWatchPath: "/logs/"}, // Must match the fullPath exactly
|
||||
},
|
||||
})
|
||||
|
||||
// Simulate a routine scan (wasIndexed=true, IsRoutineScan=true)
|
||||
idx.wasIndexed = true
|
||||
config := actionConfig{IsRoutineScan: true}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fullPath string
|
||||
baseName string
|
||||
shouldSkip bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Skip NeverWatch path during routine scan",
|
||||
fullPath: "/cache/",
|
||||
baseName: "cache",
|
||||
shouldSkip: true,
|
||||
description: "Folder with neverWatch should be skipped in routine scan",
|
||||
},
|
||||
{
|
||||
name: "Skip NeverWatch name during routine scan",
|
||||
fullPath: "/logs/",
|
||||
baseName: "logs",
|
||||
shouldSkip: true,
|
||||
description: "Folder with neverWatch name should be skipped in routine scan",
|
||||
},
|
||||
{
|
||||
name: "Don't skip regular folder",
|
||||
fullPath: "/regular/",
|
||||
baseName: "regular",
|
||||
shouldSkip: false,
|
||||
description: "Regular folder should not be skipped",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := idx.shouldSkip(true, false, tt.fullPath, tt.baseName, config)
|
||||
if result != tt.shouldSkip {
|
||||
t.Errorf("%s: expected shouldSkip=%v, got %v (fullPath=%s, baseName=%s)",
|
||||
tt.description, tt.shouldSkip, result, tt.fullPath, tt.baseName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test initial scan (IsRoutineScan=false) - NeverWatch folders should NOT be skipped
|
||||
config.IsRoutineScan = false
|
||||
result := idx.shouldSkip(true, false, "/cache/", "cache", config)
|
||||
if result != false {
|
||||
t.Errorf("NeverWatch folder should NOT be skipped during initial scan, got shouldSkip=%v", result)
|
||||
}
|
||||
}
|
||||
+220
-132
@@ -5,7 +5,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -25,17 +24,10 @@ var (
|
||||
|
||||
// actionConfig holds all configuration options for indexing operations
|
||||
type actionConfig struct {
|
||||
Quick bool // whether to perform a quick scan (skip unchanged directories)
|
||||
Recursive bool // whether to recursively index subdirectories
|
||||
ForceCheck bool // whether to check indexing skip rules.
|
||||
}
|
||||
|
||||
// NewactionConfig creates a new actionConfig with common presets
|
||||
func NewactionConfig() *actionConfig {
|
||||
return &actionConfig{
|
||||
Quick: false,
|
||||
Recursive: true,
|
||||
}
|
||||
Quick bool // whether to perform a quick scan (skip unchanged directories)
|
||||
Recursive bool // whether to recursively index subdirectories
|
||||
CheckViewable bool // whether to check if the path has viewable:true (for API access checks)
|
||||
IsRoutineScan bool // whether this is a routine/scheduled scan (vs initial indexing)
|
||||
}
|
||||
|
||||
// reduced index is json exposed to the client
|
||||
@@ -64,7 +56,7 @@ type Index struct {
|
||||
FilesChangedDuringIndexing bool `json:"-"`
|
||||
mock bool
|
||||
mu sync.RWMutex
|
||||
hasIndex bool
|
||||
wasIndexed bool
|
||||
FoundHardLinks map[string]uint64 `json:"-"` // hardlink path -> size
|
||||
processedInodes map[uint64]struct{} `json:"-"`
|
||||
totalSize uint64 `json:"-"`
|
||||
@@ -123,12 +115,12 @@ func Initialize(source *settings.Source, mock bool) {
|
||||
}
|
||||
|
||||
// indexDirectoryWithOptions wraps indexDirectory with actionConfig
|
||||
func (idx *Index) indexDirectoryWithOptions(adjustedPath string, config *actionConfig) error {
|
||||
func (idx *Index) indexDirectoryWithOptions(adjustedPath string, config actionConfig) error {
|
||||
return idx.indexDirectory(adjustedPath, config)
|
||||
}
|
||||
|
||||
// Define a function to recursively index files and directories
|
||||
func (idx *Index) indexDirectory(adjustedPath string, config *actionConfig) error {
|
||||
func (idx *Index) indexDirectory(adjustedPath string, config actionConfig) error {
|
||||
// Normalize path to always have trailing slash (except for root which is just "/")
|
||||
if adjustedPath != "/" {
|
||||
adjustedPath = strings.TrimSuffix(adjustedPath, "/") + "/"
|
||||
@@ -149,7 +141,7 @@ func (idx *Index) indexDirectory(adjustedPath string, config *actionConfig) erro
|
||||
|
||||
// check if excluded from indexing
|
||||
hidden := isHidden(dirInfo, idx.Path+adjustedPath)
|
||||
if idx.shouldSkip(dirInfo.IsDir(), hidden, adjustedPath, dirInfo.Name()) {
|
||||
if idx.shouldSkip(dirInfo.IsDir(), hidden, adjustedPath, dirInfo.Name(), config) {
|
||||
return errors.ErrNotIndexed
|
||||
}
|
||||
|
||||
@@ -182,7 +174,7 @@ func (idx *Index) indexDirectory(adjustedPath string, config *actionConfig) erro
|
||||
idx.mu.Unlock()
|
||||
} else if config.Quick {
|
||||
for _, item := range cacheDirItems {
|
||||
subConfig := &actionConfig{
|
||||
subConfig := actionConfig{
|
||||
Quick: config.Quick,
|
||||
Recursive: true,
|
||||
}
|
||||
@@ -231,6 +223,10 @@ func (idx *Index) GetFsDirInfo(adjustedPath string) (*iteminfo.FileInfo, error)
|
||||
},
|
||||
}
|
||||
fileInfo.DetectType(realPath, false)
|
||||
|
||||
// Set HasPreview flags using consolidated helper
|
||||
setFilePreviewFlags(&fileInfo.ItemInfo, realPath)
|
||||
|
||||
return &fileInfo, nil
|
||||
}
|
||||
|
||||
@@ -241,10 +237,10 @@ func (idx *Index) GetFsDirInfo(adjustedPath string) (*iteminfo.FileInfo, error)
|
||||
// adjustedPath is already normalized with trailing slash
|
||||
combinedPath := adjustedPath
|
||||
var response *iteminfo.FileInfo
|
||||
response, err = idx.GetDirInfo(dir, dirInfo, realPath, adjustedPath, combinedPath, &actionConfig{
|
||||
Quick: false,
|
||||
Recursive: false,
|
||||
ForceCheck: true,
|
||||
response, err = idx.GetDirInfo(dir, dirInfo, realPath, adjustedPath, combinedPath, actionConfig{
|
||||
Quick: false,
|
||||
Recursive: false,
|
||||
CheckViewable: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -274,7 +270,7 @@ func (idx *Index) GetFsDirInfo(adjustedPath string) (*iteminfo.FileInfo, error)
|
||||
|
||||
}
|
||||
|
||||
func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjustedPath, combinedPath string, config *actionConfig) (*iteminfo.FileInfo, error) {
|
||||
func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjustedPath, combinedPath string, config actionConfig) (*iteminfo.FileInfo, error) {
|
||||
// Ensure combinedPath has exactly one trailing slash to prevent double slashes in subdirectory paths
|
||||
combinedPath = strings.TrimRight(combinedPath, "/") + "/"
|
||||
// Read directory contents
|
||||
@@ -300,12 +296,21 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
baseName := file.Name()
|
||||
fullCombined := combinedPath + baseName
|
||||
if adjustedPath == "/" {
|
||||
if !idx.shouldInclude(isDir, combinedPath, file.Name()) {
|
||||
if !idx.shouldInclude(file.Name()) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !config.ForceCheck && idx.shouldSkip(isDir, hidden, fullCombined, baseName) {
|
||||
continue
|
||||
// Skip logic based on mode
|
||||
if config.CheckViewable {
|
||||
// When checking viewable items: skip if shouldSkip=true AND not viewable
|
||||
if idx.shouldSkip(isDir, hidden, fullCombined, baseName, config) && !idx.IsViewable(isDir, fullCombined) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Normal indexing mode: skip if shouldSkip=true
|
||||
if idx.shouldSkip(isDir, hidden, fullCombined, baseName, config) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
itemInfo := &iteminfo.ItemInfo{
|
||||
Name: file.Name(),
|
||||
@@ -315,8 +320,9 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
|
||||
if isDir {
|
||||
dirPath := combinedPath + file.Name()
|
||||
if idx.hasIndex && config.Recursive && len(idx.Config.NeverWatchPaths) > 0 {
|
||||
if slices.Contains(idx.Config.NeverWatchPaths, fullCombined) {
|
||||
// Check NeverWatchPaths map - O(1) lookup for paths with neverWatch: true
|
||||
if idx.wasIndexed && config.Recursive && idx.Config.ResolvedConditionals != nil {
|
||||
if _, exists := idx.Config.ResolvedConditionals.NeverWatchPaths[fullCombined]; exists {
|
||||
realDirInfo, exists := idx.GetMetadataInfo(dirPath, true)
|
||||
if exists {
|
||||
itemInfo.Size = realDirInfo.Size
|
||||
@@ -353,40 +359,21 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
} else {
|
||||
size, shouldCountSize := idx.handleFile(file, fullCombined)
|
||||
itemInfo.DetectType(realPath+"/"+file.Name(), false)
|
||||
simpleType := strings.Split(itemInfo.Type, "/")[0]
|
||||
if simpleType == "audio" {
|
||||
// Check if this file is new or changed by comparing with previous metadata
|
||||
shouldCheckAlbumArt := false
|
||||
previousInfo, exists := idx.GetReducedMetadata(fullCombined, false)
|
||||
if !exists {
|
||||
// File is new - check album art
|
||||
shouldCheckAlbumArt = true
|
||||
} else if previousInfo.ModTime != file.ModTime() {
|
||||
// File has been modified - check album art
|
||||
shouldCheckAlbumArt = true
|
||||
} else {
|
||||
// File unchanged - use cached album art info
|
||||
itemInfo.HasPreview = previousInfo.HasPreview
|
||||
// Set HasPreview flags - use cached metadata optimization only when indexing is enabled
|
||||
if !idx.Config.DisableIndexing && config.Recursive {
|
||||
// Optimization: For audio files during indexing, check if we can use cached album art info
|
||||
simpleType := strings.Split(itemInfo.Type, "/")[0]
|
||||
if simpleType == "audio" {
|
||||
previousInfo, exists := idx.GetReducedMetadata(fullCombined, false)
|
||||
if exists && time.Time.Equal(previousInfo.ModTime, file.ModTime()) {
|
||||
// File unchanged - use cached album art info
|
||||
itemInfo.HasPreview = previousInfo.HasPreview
|
||||
}
|
||||
}
|
||||
}
|
||||
// When indexing is disabled or CheckViewable mode, always check directly
|
||||
setFilePreviewFlags(itemInfo, realPath+"/"+file.Name())
|
||||
|
||||
if shouldCheckAlbumArt {
|
||||
itemInfo.HasPreview = iteminfo.HasAlbumArt(realPath+"/"+file.Name(), filepath.Ext(file.Name()))
|
||||
}
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(file.Name()))
|
||||
extWithoutPeriod := strings.TrimPrefix(ext, ".")
|
||||
if simpleType == "image" {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
switch extWithoutPeriod {
|
||||
case "heic", "heif":
|
||||
if settings.CanConvertImage(extWithoutPeriod) {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
}
|
||||
if simpleType == "video" && settings.CanConvertVideo(extWithoutPeriod) {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
itemInfo.Size = int64(size)
|
||||
|
||||
// Update parent folder preview status for images, videos, and audio with album art
|
||||
@@ -395,15 +382,6 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
hasPreview = true
|
||||
}
|
||||
|
||||
// Set HasPreview for office docs and PDFs
|
||||
// These files are previewable but DON'T set parent folder preview
|
||||
if settings.Config.Integrations.OnlyOffice.Secret != "" && iteminfo.IsOnlyOffice(file.Name()) {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
if iteminfo.HasDocConvertableExtension(itemInfo.Name, itemInfo.Type) {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
|
||||
fileInfos = append(fileInfos, *itemInfo)
|
||||
if shouldCountSize {
|
||||
totalSize += itemInfo.Size
|
||||
@@ -414,7 +392,7 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
}
|
||||
}
|
||||
|
||||
if totalSize == 0 && idx.Config.Exclude.ZeroSizeFolders {
|
||||
if totalSize == 0 && idx.Config.Conditionals.ZeroSizeFolders {
|
||||
return nil, errors.ErrNotIndexed
|
||||
}
|
||||
|
||||
@@ -485,7 +463,7 @@ func (idx *Index) GetRealPath(relativePath ...string) (string, bool, error) {
|
||||
}
|
||||
|
||||
func (idx *Index) RefreshFileInfo(opts utils.FileOptions) error {
|
||||
config := &actionConfig{
|
||||
config := actionConfig{
|
||||
Quick: false,
|
||||
Recursive: opts.Recursive,
|
||||
}
|
||||
@@ -536,102 +514,194 @@ func isHidden(file os.FileInfo, srcPath string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (idx *Index) shouldInclude(isDir bool, fullCombined, baseName string) bool {
|
||||
rules := idx.Config.Include
|
||||
hasRules := false
|
||||
if len(rules.RootFolders) > 0 {
|
||||
hasRules = true
|
||||
for _, p := range rules.RootFolders {
|
||||
if strings.HasPrefix(fullCombined, p) {
|
||||
// setFilePreviewFlags determines if a file should have a preview based on its type
|
||||
// This consolidates the logic used in both GetFsDirInfo and GetDirInfo
|
||||
func setFilePreviewFlags(fileInfo *iteminfo.ItemInfo, realPath string) {
|
||||
simpleType := strings.Split(fileInfo.Type, "/")[0]
|
||||
ext := strings.ToLower(filepath.Ext(fileInfo.Name))
|
||||
extWithoutPeriod := strings.TrimPrefix(ext, ".")
|
||||
|
||||
// Check if it's an image
|
||||
if simpleType == "image" {
|
||||
fileInfo.HasPreview = true
|
||||
}
|
||||
|
||||
// Check for HEIC/HEIF
|
||||
switch extWithoutPeriod {
|
||||
case "heic", "heif":
|
||||
if settings.CanConvertImage(extWithoutPeriod) {
|
||||
fileInfo.HasPreview = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a video
|
||||
if simpleType == "video" && settings.CanConvertVideo(extWithoutPeriod) {
|
||||
fileInfo.HasPreview = true
|
||||
}
|
||||
|
||||
// Check for audio with album art (always check, don't rely on cache)
|
||||
if simpleType == "audio" {
|
||||
fileInfo.HasPreview = iteminfo.HasAlbumArt(realPath, ext)
|
||||
}
|
||||
|
||||
// Check for office docs and PDFs
|
||||
if settings.Config.Integrations.OnlyOffice.Secret != "" && iteminfo.IsOnlyOffice(fileInfo.Name) {
|
||||
fileInfo.HasPreview = true
|
||||
}
|
||||
if iteminfo.HasDocConvertableExtension(fileInfo.Name, fileInfo.Type) {
|
||||
fileInfo.HasPreview = true
|
||||
}
|
||||
}
|
||||
|
||||
// IsViewable checks if a path has viewable:true (allows FS access without indexing)
|
||||
func (idx *Index) IsViewable(isDir bool, adjustedPath string) bool {
|
||||
rules := idx.Config.ResolvedConditionals
|
||||
|
||||
baseName := filepath.Base(strings.TrimSuffix(adjustedPath, "/"))
|
||||
|
||||
if isDir {
|
||||
// Exact match (O(1))
|
||||
if rule, exists := rules.FolderNames[baseName]; exists && rule.Viewable {
|
||||
return true
|
||||
}
|
||||
// Prefix/suffix (O(n))
|
||||
for _, rule := range rules.FolderPaths {
|
||||
if strings.HasPrefix(adjustedPath, rule.FolderPath) && rule.Viewable {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rules.RootFiles) > 0 {
|
||||
hasRules = true
|
||||
for _, p := range rules.RootFiles {
|
||||
if strings.HasPrefix(fullCombined, p) {
|
||||
for _, rule := range rules.FolderEndsWith {
|
||||
if strings.HasSuffix(baseName, rule.FolderEndsWith) && rule.Viewable {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, rule := range rules.FolderStartsWith {
|
||||
if strings.HasPrefix(baseName, rule.FolderStartsWith) && rule.Viewable {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Exact match (O(1))
|
||||
if rule, exists := rules.FileNames[baseName]; exists && rule.Viewable {
|
||||
return true
|
||||
}
|
||||
// Prefix/suffix (O(n))
|
||||
for _, rule := range rules.FilePaths {
|
||||
if strings.HasPrefix(adjustedPath, rule.FilePath) && rule.Viewable {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, rule := range rules.FileEndsWith {
|
||||
if strings.HasSuffix(baseName, rule.FileEndsWith) && rule.Viewable {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, rule := range rules.FileStartsWith {
|
||||
if strings.HasPrefix(baseName, rule.FileStartsWith) && rule.Viewable {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasRules {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined, baseName string) bool {
|
||||
func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined, baseName string, config actionConfig) bool {
|
||||
rules := idx.Config.ResolvedConditionals
|
||||
if rules == nil {
|
||||
rules = &settings.ResolvedConditionalsConfig{}
|
||||
}
|
||||
if fullCombined == "/" {
|
||||
return false
|
||||
}
|
||||
// When indexing is disabled globally, behavior depends on the mode
|
||||
if idx.Config.DisableIndexing {
|
||||
// If checking viewable (filesystem access), don't skip - show everything from filesystem
|
||||
if config.CheckViewable {
|
||||
return false
|
||||
}
|
||||
// If indexing mode, skip everything
|
||||
return true
|
||||
}
|
||||
|
||||
rules := idx.Config.Exclude
|
||||
|
||||
if isDir {
|
||||
if len(rules.FolderPaths) > 0 {
|
||||
for _, p := range rules.FolderPaths {
|
||||
if strings.HasPrefix(fullCombined, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rules.FolderNames) > 0 && slices.Contains(rules.FolderNames, baseName) {
|
||||
if isDir && config.IsRoutineScan {
|
||||
// Check NeverWatch: Skip directories with index=true AND neverWatch=true during routine scans
|
||||
// This allows them to be indexed once but never re-scanned
|
||||
_, ok := rules.NeverWatchPaths[fullCombined]
|
||||
if ok {
|
||||
// skip scanning, but is viewable
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if isDir {
|
||||
if rule, ok := rules.FolderNames[baseName]; ok {
|
||||
if _, ok := rules.FolderPaths[fullCombined]; !ok {
|
||||
// create a rule so sub folders can be skipped
|
||||
rules.FolderPaths[fullCombined] = rule
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Check FolderPaths: Two-stage check for performance
|
||||
// Stage 1: O(1) exact match (fast path for the folder itself)
|
||||
if _, ok := rules.FolderPaths[fullCombined]; ok {
|
||||
return true
|
||||
}
|
||||
// Stage 2: O(n) prefix match (for child folders)
|
||||
for path := range rules.FolderPaths {
|
||||
if strings.HasPrefix(fullCombined, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check FolderEndsWith (suffix match on base name) - use original slice
|
||||
if len(rules.FolderEndsWith) > 0 {
|
||||
for _, end := range rules.FolderEndsWith {
|
||||
if strings.HasSuffix(baseName, end) {
|
||||
for _, rule := range rules.FolderEndsWith {
|
||||
if hasSuffix := strings.HasSuffix(baseName, rule.FolderEndsWith); hasSuffix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rules.FolderStartsWith) > 0 {
|
||||
for _, start := range rules.FolderStartsWith {
|
||||
if strings.HasPrefix(baseName, start) {
|
||||
return true
|
||||
}
|
||||
for _, rule := range rules.FolderStartsWith {
|
||||
if hasPrefix := strings.HasPrefix(baseName, rule.FolderStartsWith); hasPrefix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(rules.FilePaths) > 0 {
|
||||
for _, p := range rules.FilePaths {
|
||||
if strings.HasPrefix(fullCombined, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rules.FileNames) > 0 && slices.Contains(rules.FileNames, baseName) {
|
||||
// Check FileNames (exact match on base name) - O(1) lookup
|
||||
if _, ok := rules.FileNames[baseName]; ok {
|
||||
return true
|
||||
}
|
||||
if len(rules.FileEndsWith) > 0 {
|
||||
for _, end := range rules.FileEndsWith {
|
||||
if strings.HasSuffix(baseName, end) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check FilePaths: Two-stage check for performance
|
||||
// Stage 1: O(1) exact match (fast path for the file itself)
|
||||
if _, ok := rules.FilePaths[fullCombined]; ok {
|
||||
return true
|
||||
}
|
||||
// Stage 2: O(n) prefix match (for files in excluded folders)
|
||||
for path := range rules.FilePaths {
|
||||
if strings.HasPrefix(fullCombined, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if len(rules.FileStartsWith) > 0 {
|
||||
for _, start := range rules.FileStartsWith {
|
||||
if strings.HasPrefix(baseName, start) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, rule := range rules.FileEndsWith {
|
||||
if hasSuffix := strings.HasSuffix(baseName, rule.FileEndsWith); hasSuffix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Exclude if parent directory matches FolderPaths
|
||||
if len(rules.FolderPaths) > 0 {
|
||||
parent := filepath.Dir(fullCombined)
|
||||
for _, p := range rules.FolderPaths {
|
||||
if strings.HasPrefix(parent, p) {
|
||||
return true
|
||||
}
|
||||
for _, rule := range rules.FileStartsWith {
|
||||
if hasPrefix := strings.HasPrefix(baseName, rule.FileStartsWith); hasPrefix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if rules.Hidden && isHidden {
|
||||
if idx.Config.Conditionals.Hidden && isHidden {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -649,7 +719,7 @@ func (idx *Index) SetUsage(totalBytes uint64) {
|
||||
idx.DiskTotal = totalBytes
|
||||
}
|
||||
|
||||
func (idx *Index) SetStatus(status IndexStatus) {
|
||||
func (idx *Index) SetStatus(status IndexStatus) error {
|
||||
idx.mu.Lock()
|
||||
idx.Status = status
|
||||
switch status {
|
||||
@@ -659,7 +729,7 @@ func (idx *Index) SetStatus(status IndexStatus) {
|
||||
idx.runningScannerCount = 0
|
||||
}
|
||||
idx.mu.Unlock()
|
||||
idx.SendSourceUpdateEvent()
|
||||
return idx.SendSourceUpdateEvent()
|
||||
}
|
||||
|
||||
func (idx *Index) handleFile(file os.FileInfo, fullCombined string) (size uint64, shouldCountSize bool) {
|
||||
@@ -709,3 +779,21 @@ func (idx *Index) MakeIndexPath(path string) string {
|
||||
path = strings.TrimSuffix(path, "/") + "/"
|
||||
return path
|
||||
}
|
||||
|
||||
func (idx *Index) shouldInclude(baseName string) bool {
|
||||
rules := idx.Config.ResolvedConditionals
|
||||
if rules == nil {
|
||||
rules = &settings.ResolvedConditionalsConfig{}
|
||||
}
|
||||
hasRules := false
|
||||
if len(rules.IncludeRootItems) > 0 {
|
||||
hasRules = true
|
||||
if _, exists := rules.IncludeRootItems["/"+baseName]; exists {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if !hasRules {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func setupTestIndex(t *testing.T) (*Index, string, func()) {
|
||||
DisableIndexing: false,
|
||||
},
|
||||
},
|
||||
hasIndex: true,
|
||||
wasIndexed: true,
|
||||
mock: true, // Enable mock mode
|
||||
Directories: make(map[string]*iteminfo.FileInfo),
|
||||
DirectoriesLedger: make(map[string]struct{}),
|
||||
|
||||
@@ -49,19 +49,20 @@ func (idx *Index) newScanner(origin string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (idx *Index) PreScan() {
|
||||
idx.SetStatus(INDEXING)
|
||||
func (idx *Index) PreScan() error {
|
||||
return idx.SetStatus(INDEXING)
|
||||
}
|
||||
|
||||
func (idx *Index) PostScan() {
|
||||
func (idx *Index) PostScan() error {
|
||||
idx.mu.Lock()
|
||||
idx.garbageCollection()
|
||||
idx.hasIndex = true
|
||||
idx.wasIndexed = true
|
||||
idx.runningScannerCount--
|
||||
idx.mu.Unlock()
|
||||
if idx.runningScannerCount == 0 {
|
||||
idx.SetStatus(READY)
|
||||
return idx.SetStatus(READY)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (idx *Index) garbageCollection() {
|
||||
@@ -104,25 +105,24 @@ func (idx *Index) UpdateSchedule() {
|
||||
}
|
||||
}
|
||||
|
||||
func (idx *Index) SendSourceUpdateEvent() {
|
||||
func (idx *Index) SendSourceUpdateEvent() error {
|
||||
if idx.mock {
|
||||
logger.Debug("Skipping source update event for mock index.")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
reducedIndex, err := GetIndexInfo(idx.Name)
|
||||
if err != nil {
|
||||
logger.Errorf("Error getting index info: %v", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
sourceAsMap := map[string]ReducedIndex{
|
||||
idx.Name: reducedIndex,
|
||||
}
|
||||
message, err := json.Marshal(sourceAsMap)
|
||||
if err != nil {
|
||||
logger.Errorf("Error marshalling source update message: %v", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
events.SendSourceUpdate(idx.Name, string(message))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (idx *Index) RunIndexing(origin string, quick bool) {
|
||||
@@ -130,7 +130,11 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
|
||||
logger.Debugf("Indexing already in progress for [%v]", idx.Name)
|
||||
return
|
||||
}
|
||||
idx.PreScan()
|
||||
err := idx.PreScan()
|
||||
if err != nil {
|
||||
logger.Errorf("Error during indexing: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
prevNumDirs := idx.NumDirs
|
||||
prevNumFiles := idx.NumFiles
|
||||
@@ -148,11 +152,12 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
|
||||
startTime := time.Now()
|
||||
idx.FilesChangedDuringIndexing = false
|
||||
// Perform the indexing operation
|
||||
config := &actionConfig{
|
||||
Quick: quick,
|
||||
Recursive: true,
|
||||
config := actionConfig{
|
||||
Quick: quick,
|
||||
Recursive: true,
|
||||
IsRoutineScan: idx.wasIndexed, // This is a routine scan if we already have an index
|
||||
}
|
||||
err := idx.indexDirectory("/", config)
|
||||
err = idx.indexDirectory("/", config)
|
||||
if err != nil {
|
||||
logger.Errorf("Error during indexing: %v", err)
|
||||
}
|
||||
@@ -187,7 +192,11 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
|
||||
logger.Debugf("Time spent indexing [%v]: %v seconds", idx.Name, idx.FullScanTime)
|
||||
}
|
||||
|
||||
idx.PostScan()
|
||||
err = idx.PostScan()
|
||||
if err != nil {
|
||||
logger.Errorf("Error during post scan indexing: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (idx *Index) setupIndexingScanners() {
|
||||
|
||||
@@ -124,6 +124,8 @@ func TestMakeIndexPathRoot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Update these tests to use new ConditionalFilter structure
|
||||
/*
|
||||
func TestCheckIndexExclude(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -165,14 +167,16 @@ func TestCheckIndexExclude(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
result := idx.shouldSkip(tc.isDir, tc.isHidden, tc.fullPath, tc.baseName)
|
||||
result := idx.shouldSkip(tc.isDir, tc.isHidden, tc.fullPath, tc.baseName, nil)
|
||||
if result != tc.expect {
|
||||
t.Errorf("shouldSkip(%v, %v, %q, %q) = %v; want %v", tc.isDir, tc.isHidden, tc.fullPath, tc.baseName, result, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func TestCheckIndexInclude(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -207,10 +211,11 @@ func TestCheckIndexInclude(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
result := idx.shouldInclude(tc.isDir, tc.fullPath, tc.baseName)
|
||||
if result != tc.expect {
|
||||
t.Errorf("shouldInclude(%v, %q, %q) = %v; want %v", tc.isDir, tc.fullPath, tc.baseName, result, tc.expect)
|
||||
}
|
||||
})
|
||||
result := idx.shouldInclude(tc.isDir, tc.fullPath, tc.baseName)
|
||||
if result != tc.expect {
|
||||
t.Errorf("shouldInclude(%v, %q, %q) = %v; want %v", tc.isDir, tc.fullPath, tc.baseName, result, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -167,8 +167,9 @@ func GetIndexInfo(sourceName string) (ReducedIndex, error) {
|
||||
if !ok {
|
||||
totalBytes, err := getPartitionSize(sourcePath)
|
||||
if err != nil {
|
||||
logger.Errorf("error getting disk usage for %s: %v", sourcePath, err)
|
||||
idx.SetStatus(UNAVAILABLE)
|
||||
idx.mu.Lock()
|
||||
idx.Status = UNAVAILABLE
|
||||
idx.mu.Unlock()
|
||||
return ReducedIndex{}, fmt.Errorf("error getting disk usage for %s: %v", sourcePath, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package indexing
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package indexing
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build mupdf
|
||||
// +build mupdf
|
||||
|
||||
package preview
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !mupdf
|
||||
// +build !mupdf
|
||||
|
||||
package preview
|
||||
|
||||
|
||||
+218
-123
@@ -2320,6 +2320,91 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"/public/api/resources": {
|
||||
"put": {
|
||||
"description": "Updates the content of a file in a public share.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Public Shares"
|
||||
],
|
||||
"summary": "Update a file in a public share",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Share hash for authentication",
|
||||
"name": "hash",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Path to the file to update",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "New content for the file",
|
||||
"name": "content",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "File updated successfully",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request or parameters",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Share unavailable or update not allowed",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Share not found or file not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Handles file and directory uploads to an upload-only public share. Supports chunked uploads, conflict resolution (override), and directory creation.",
|
||||
"consumes": [
|
||||
@@ -2833,6 +2918,75 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ConditionalFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hidden": {
|
||||
"description": "exclude hidden files and folders.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ignoreZeroSizeFolders": {
|
||||
"description": "ignore folders with 0 size",
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"description": "list of item rules to apply to specific paths",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/settings.ConditionalIndexConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ConditionalIndexConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileEndsWith": {
|
||||
"description": "(global) exclude files that end with these suffixes. Eg. \".jpg\" or \".txt\"",
|
||||
"type": "string"
|
||||
},
|
||||
"fileNames": {
|
||||
"description": "(global) exclude files that match these names. Eg. \"file.txt\" or \"test.csv\"",
|
||||
"type": "string"
|
||||
},
|
||||
"filePath": {
|
||||
"description": "(global) exclude files that match this path. Eg. \"/path/to/file.txt\" or \"/path/to/file.txt/subfile.txt\"",
|
||||
"type": "string"
|
||||
},
|
||||
"fileStartsWith": {
|
||||
"description": "(global) exclude files that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "string"
|
||||
},
|
||||
"folderEndsWith": {
|
||||
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
|
||||
"type": "string"
|
||||
},
|
||||
"folderNames": {
|
||||
"description": "(global) exclude folders that match these names. Eg. \"folder\" or \"subfolder\"",
|
||||
"type": "string"
|
||||
},
|
||||
"folderPath": {
|
||||
"description": "(global) exclude folders that match this path. Eg. \"/path/to/folder\" or \"/path/to/folder/subfolder\"",
|
||||
"type": "string"
|
||||
},
|
||||
"folderStartsWith": {
|
||||
"description": "(global) exclude folders that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "string"
|
||||
},
|
||||
"includeRootItem": {
|
||||
"description": "include only these items at root folder level",
|
||||
"type": "string"
|
||||
},
|
||||
"neverWatchPath": {
|
||||
"description": "index the folder in the first pass to get included in search, but never re-indexed.",
|
||||
"type": "string"
|
||||
},
|
||||
"viewable": {
|
||||
"description": "Enable viewing in UI but exclude from indexing",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.CustomTheme": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2846,75 +3000,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ExcludeIndexFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileEndsWith": {
|
||||
"description": "(global) exclude files that end with these suffixes. Eg. \".jpg\" or \".txt\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fileNames": {
|
||||
"description": "(global) list of file names to include/exclude. Eg. \"a.jpg\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filePaths": {
|
||||
"description": "list of filepaths Eg. \"folder1\" or \"file1.txt\" or \"folder1/file1.txt\" (without source path prefix)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fileStartsWith": {
|
||||
"description": "(global) exclude files that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderEndsWith": {
|
||||
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderNames": {
|
||||
"description": "(global) list of folder names to include/exclude. Eg. \"@eadir\" or \".thumbnails\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderPaths": {
|
||||
"description": "(filepath) list of folder names to include/exclude. Eg. \"folder1\" or \"folder1/subfolder\" (do not include source path, just the subpaths from the source path)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderStartsWith": {
|
||||
"description": "(global) exclude folders that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hidden": {
|
||||
"description": "exclude hidden files and folders.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ignoreZeroSizeFolders": {
|
||||
"description": "ignore folders with 0 size",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ExternalLink": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -3010,25 +3095,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.IncludeIndexFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rootFiles": {
|
||||
"description": "list of root files to include, relative to the source path (eg. \"file1.txt\")",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"rootFolders": {
|
||||
"description": "list of root folders to include, relative to the source path (eg. \"folder1\")",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.Integrations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3387,6 +3453,14 @@ const docTemplate = `{
|
||||
"settings.SourceConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conditionals": {
|
||||
"description": "conditional rules to apply when indexing to include/exclude certain items",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/settings.ConditionalFilter"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createUserDir": {
|
||||
"description": "create a user directory for each user",
|
||||
"type": "boolean"
|
||||
@@ -3411,41 +3485,10 @@ const docTemplate = `{
|
||||
"description": "disable the source, this is useful so you don't need to remove it from the config file",
|
||||
"type": "boolean"
|
||||
},
|
||||
"exclude": {
|
||||
"description": "exclude files and folders from indexing, if include is not set",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/settings.ExcludeIndexFilter"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": {
|
||||
"description": "include files and folders from indexing, if exclude is not set",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/settings.IncludeIndexFilter"
|
||||
}
|
||||
]
|
||||
},
|
||||
"indexAlbumArt": {
|
||||
"description": "deprecated: always enabled since 0.8.6",
|
||||
"type": "boolean"
|
||||
},
|
||||
"indexingIntervalMinutes": {
|
||||
"description": "optional manual overide interval in minutes to re-index the source",
|
||||
"type": "integer"
|
||||
},
|
||||
"maxWatchers": {
|
||||
"description": "number of concurrent watchers to use for this source, currently not supported",
|
||||
"type": "integer"
|
||||
},
|
||||
"neverWatchPaths": {
|
||||
"description": "paths that get initially once. Useful for folders that rarely change contents (without source path prefix)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"private": {
|
||||
"description": "designate as source as private -- currently just means no sharing permitted.",
|
||||
"type": "boolean"
|
||||
@@ -3606,8 +3649,19 @@ const docTemplate = `{
|
||||
"share.CreateBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowUpload": {
|
||||
"description": "AllowEdit bool ` + "`" + `json:\"allowEdit,omitempty\"` + "`" + `",
|
||||
"allowCreate": {
|
||||
"description": "allow creating files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowDelete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowModify": {
|
||||
"description": "allow modifying files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowReplacements": {
|
||||
"description": "allow replacements of files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowedUsernames": {
|
||||
@@ -3625,7 +3679,12 @@ const docTemplate = `{
|
||||
"disableAnonymous": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableDownload": {
|
||||
"description": "don't allow downloading files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableFileViewer": {
|
||||
"description": "don't allow viewing files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableShareCard": {
|
||||
@@ -3716,8 +3775,19 @@ const docTemplate = `{
|
||||
"share.Link": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowUpload": {
|
||||
"description": "AllowEdit bool ` + "`" + `json:\"allowEdit,omitempty\"` + "`" + `",
|
||||
"allowCreate": {
|
||||
"description": "allow creating files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowDelete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowModify": {
|
||||
"description": "allow modifying files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowReplacements": {
|
||||
"description": "allow replacements of files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowedUsernames": {
|
||||
@@ -3735,7 +3805,12 @@ const docTemplate = `{
|
||||
"disableAnonymous": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableDownload": {
|
||||
"description": "don't allow downloading files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableFileViewer": {
|
||||
"description": "don't allow viewing files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableShareCard": {
|
||||
@@ -3884,18 +3959,35 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"admin": {
|
||||
"description": "allow admin access",
|
||||
"type": "boolean"
|
||||
},
|
||||
"api": {
|
||||
"description": "allow api access",
|
||||
"type": "boolean"
|
||||
},
|
||||
"create": {
|
||||
"description": "allow creating or uploading files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"delete": {
|
||||
"description": "allow deleting files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"download": {
|
||||
"description": "allow downloading files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"modify": {
|
||||
"description": "allow modifying files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"realtime": {
|
||||
"description": "allow realtime updates",
|
||||
"type": "boolean"
|
||||
},
|
||||
"share": {
|
||||
"description": "allow sharing files",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
@@ -4130,6 +4222,9 @@ const docTemplate = `{
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"viewMode": {
|
||||
"description": "view mode to use: eg. normal, list, grid, or compact",
|
||||
"type": "string"
|
||||
|
||||
+218
-123
@@ -2309,6 +2309,91 @@
|
||||
}
|
||||
},
|
||||
"/public/api/resources": {
|
||||
"put": {
|
||||
"description": "Updates the content of a file in a public share.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Public Shares"
|
||||
],
|
||||
"summary": "Update a file in a public share",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Share hash for authentication",
|
||||
"name": "hash",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Path to the file to update",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "New content for the file",
|
||||
"name": "content",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "File updated successfully",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request or parameters",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Share unavailable or update not allowed",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Share not found or file not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Handles file and directory uploads to an upload-only public share. Supports chunked uploads, conflict resolution (override), and directory creation.",
|
||||
"consumes": [
|
||||
@@ -2822,6 +2907,75 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ConditionalFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hidden": {
|
||||
"description": "exclude hidden files and folders.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ignoreZeroSizeFolders": {
|
||||
"description": "ignore folders with 0 size",
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"description": "list of item rules to apply to specific paths",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/settings.ConditionalIndexConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ConditionalIndexConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileEndsWith": {
|
||||
"description": "(global) exclude files that end with these suffixes. Eg. \".jpg\" or \".txt\"",
|
||||
"type": "string"
|
||||
},
|
||||
"fileNames": {
|
||||
"description": "(global) exclude files that match these names. Eg. \"file.txt\" or \"test.csv\"",
|
||||
"type": "string"
|
||||
},
|
||||
"filePath": {
|
||||
"description": "(global) exclude files that match this path. Eg. \"/path/to/file.txt\" or \"/path/to/file.txt/subfile.txt\"",
|
||||
"type": "string"
|
||||
},
|
||||
"fileStartsWith": {
|
||||
"description": "(global) exclude files that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "string"
|
||||
},
|
||||
"folderEndsWith": {
|
||||
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
|
||||
"type": "string"
|
||||
},
|
||||
"folderNames": {
|
||||
"description": "(global) exclude folders that match these names. Eg. \"folder\" or \"subfolder\"",
|
||||
"type": "string"
|
||||
},
|
||||
"folderPath": {
|
||||
"description": "(global) exclude folders that match this path. Eg. \"/path/to/folder\" or \"/path/to/folder/subfolder\"",
|
||||
"type": "string"
|
||||
},
|
||||
"folderStartsWith": {
|
||||
"description": "(global) exclude folders that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "string"
|
||||
},
|
||||
"includeRootItem": {
|
||||
"description": "include only these items at root folder level",
|
||||
"type": "string"
|
||||
},
|
||||
"neverWatchPath": {
|
||||
"description": "index the folder in the first pass to get included in search, but never re-indexed.",
|
||||
"type": "string"
|
||||
},
|
||||
"viewable": {
|
||||
"description": "Enable viewing in UI but exclude from indexing",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.CustomTheme": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2835,75 +2989,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ExcludeIndexFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileEndsWith": {
|
||||
"description": "(global) exclude files that end with these suffixes. Eg. \".jpg\" or \".txt\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fileNames": {
|
||||
"description": "(global) list of file names to include/exclude. Eg. \"a.jpg\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"filePaths": {
|
||||
"description": "list of filepaths Eg. \"folder1\" or \"file1.txt\" or \"folder1/file1.txt\" (without source path prefix)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fileStartsWith": {
|
||||
"description": "(global) exclude files that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderEndsWith": {
|
||||
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderNames": {
|
||||
"description": "(global) list of folder names to include/exclude. Eg. \"@eadir\" or \".thumbnails\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderPaths": {
|
||||
"description": "(filepath) list of folder names to include/exclude. Eg. \"folder1\" or \"folder1/subfolder\" (do not include source path, just the subpaths from the source path)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderStartsWith": {
|
||||
"description": "(global) exclude folders that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hidden": {
|
||||
"description": "exclude hidden files and folders.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ignoreZeroSizeFolders": {
|
||||
"description": "ignore folders with 0 size",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ExternalLink": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -2999,25 +3084,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.IncludeIndexFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rootFiles": {
|
||||
"description": "list of root files to include, relative to the source path (eg. \"file1.txt\")",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"rootFolders": {
|
||||
"description": "list of root folders to include, relative to the source path (eg. \"folder1\")",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.Integrations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3376,6 +3442,14 @@
|
||||
"settings.SourceConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conditionals": {
|
||||
"description": "conditional rules to apply when indexing to include/exclude certain items",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/settings.ConditionalFilter"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createUserDir": {
|
||||
"description": "create a user directory for each user",
|
||||
"type": "boolean"
|
||||
@@ -3400,41 +3474,10 @@
|
||||
"description": "disable the source, this is useful so you don't need to remove it from the config file",
|
||||
"type": "boolean"
|
||||
},
|
||||
"exclude": {
|
||||
"description": "exclude files and folders from indexing, if include is not set",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/settings.ExcludeIndexFilter"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": {
|
||||
"description": "include files and folders from indexing, if exclude is not set",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/settings.IncludeIndexFilter"
|
||||
}
|
||||
]
|
||||
},
|
||||
"indexAlbumArt": {
|
||||
"description": "deprecated: always enabled since 0.8.6",
|
||||
"type": "boolean"
|
||||
},
|
||||
"indexingIntervalMinutes": {
|
||||
"description": "optional manual overide interval in minutes to re-index the source",
|
||||
"type": "integer"
|
||||
},
|
||||
"maxWatchers": {
|
||||
"description": "number of concurrent watchers to use for this source, currently not supported",
|
||||
"type": "integer"
|
||||
},
|
||||
"neverWatchPaths": {
|
||||
"description": "paths that get initially once. Useful for folders that rarely change contents (without source path prefix)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"private": {
|
||||
"description": "designate as source as private -- currently just means no sharing permitted.",
|
||||
"type": "boolean"
|
||||
@@ -3595,8 +3638,19 @@
|
||||
"share.CreateBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowUpload": {
|
||||
"description": "AllowEdit bool `json:\"allowEdit,omitempty\"`",
|
||||
"allowCreate": {
|
||||
"description": "allow creating files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowDelete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowModify": {
|
||||
"description": "allow modifying files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowReplacements": {
|
||||
"description": "allow replacements of files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowedUsernames": {
|
||||
@@ -3614,7 +3668,12 @@
|
||||
"disableAnonymous": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableDownload": {
|
||||
"description": "don't allow downloading files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableFileViewer": {
|
||||
"description": "don't allow viewing files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableShareCard": {
|
||||
@@ -3705,8 +3764,19 @@
|
||||
"share.Link": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowUpload": {
|
||||
"description": "AllowEdit bool `json:\"allowEdit,omitempty\"`",
|
||||
"allowCreate": {
|
||||
"description": "allow creating files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowDelete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowModify": {
|
||||
"description": "allow modifying files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowReplacements": {
|
||||
"description": "allow replacements of files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowedUsernames": {
|
||||
@@ -3724,7 +3794,12 @@
|
||||
"disableAnonymous": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableDownload": {
|
||||
"description": "don't allow downloading files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableFileViewer": {
|
||||
"description": "don't allow viewing files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableShareCard": {
|
||||
@@ -3873,18 +3948,35 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"admin": {
|
||||
"description": "allow admin access",
|
||||
"type": "boolean"
|
||||
},
|
||||
"api": {
|
||||
"description": "allow api access",
|
||||
"type": "boolean"
|
||||
},
|
||||
"create": {
|
||||
"description": "allow creating or uploading files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"delete": {
|
||||
"description": "allow deleting files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"download": {
|
||||
"description": "allow downloading files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"modify": {
|
||||
"description": "allow modifying files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"realtime": {
|
||||
"description": "allow realtime updates",
|
||||
"type": "boolean"
|
||||
},
|
||||
"share": {
|
||||
"description": "allow sharing files",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
@@ -4119,6 +4211,9 @@
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"viewMode": {
|
||||
"description": "view mode to use: eg. normal, list, grid, or compact",
|
||||
"type": "string"
|
||||
|
||||
@@ -167,6 +167,65 @@ definitions:
|
||||
description: 'secret: secret used to encrypt TOTP secrets'
|
||||
type: string
|
||||
type: object
|
||||
settings.ConditionalFilter:
|
||||
properties:
|
||||
hidden:
|
||||
description: exclude hidden files and folders.
|
||||
type: boolean
|
||||
ignoreZeroSizeFolders:
|
||||
description: ignore folders with 0 size
|
||||
type: boolean
|
||||
rules:
|
||||
description: list of item rules to apply to specific paths
|
||||
items:
|
||||
$ref: '#/definitions/settings.ConditionalIndexConfig'
|
||||
type: array
|
||||
type: object
|
||||
settings.ConditionalIndexConfig:
|
||||
properties:
|
||||
fileEndsWith:
|
||||
description: (global) exclude files that end with these suffixes. Eg. ".jpg"
|
||||
or ".txt"
|
||||
type: string
|
||||
fileNames:
|
||||
description: (global) exclude files that match these names. Eg. "file.txt"
|
||||
or "test.csv"
|
||||
type: string
|
||||
filePath:
|
||||
description: (global) exclude files that match this path. Eg. "/path/to/file.txt"
|
||||
or "/path/to/file.txt/subfile.txt"
|
||||
type: string
|
||||
fileStartsWith:
|
||||
description: (global) exclude files that start with these prefixes. Eg. "archive-"
|
||||
or "backup-"
|
||||
type: string
|
||||
folderEndsWith:
|
||||
description: (global) exclude folders that end with these suffixes. Eg. ".thumbnails"
|
||||
or ".git"
|
||||
type: string
|
||||
folderNames:
|
||||
description: (global) exclude folders that match these names. Eg. "folder"
|
||||
or "subfolder"
|
||||
type: string
|
||||
folderPath:
|
||||
description: (global) exclude folders that match this path. Eg. "/path/to/folder"
|
||||
or "/path/to/folder/subfolder"
|
||||
type: string
|
||||
folderStartsWith:
|
||||
description: (global) exclude folders that start with these prefixes. Eg.
|
||||
"archive-" or "backup-"
|
||||
type: string
|
||||
includeRootItem:
|
||||
description: include only these items at root folder level
|
||||
type: string
|
||||
neverWatchPath:
|
||||
description: index the folder in the first pass to get included in search,
|
||||
but never re-indexed.
|
||||
type: string
|
||||
viewable:
|
||||
description: Enable viewing in UI but exclude from indexing
|
||||
type: boolean
|
||||
type: object
|
||||
settings.CustomTheme:
|
||||
properties:
|
||||
css:
|
||||
@@ -176,63 +235,6 @@ definitions:
|
||||
description: The description of the theme to display in the UI.
|
||||
type: string
|
||||
type: object
|
||||
settings.ExcludeIndexFilter:
|
||||
properties:
|
||||
fileEndsWith:
|
||||
description: (global) exclude files that end with these suffixes. Eg. ".jpg"
|
||||
or ".txt"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
fileNames:
|
||||
description: (global) list of file names to include/exclude. Eg. "a.jpg"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
filePaths:
|
||||
description: list of filepaths Eg. "folder1" or "file1.txt" or "folder1/file1.txt"
|
||||
(without source path prefix)
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
fileStartsWith:
|
||||
description: (global) exclude files that start with these prefixes. Eg. "archive-"
|
||||
or "backup-"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
folderEndsWith:
|
||||
description: (global) exclude folders that end with these suffixes. Eg. ".thumbnails"
|
||||
or ".git"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
folderNames:
|
||||
description: (global) list of folder names to include/exclude. Eg. "@eadir"
|
||||
or ".thumbnails"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
folderPaths:
|
||||
description: (filepath) list of folder names to include/exclude. Eg. "folder1"
|
||||
or "folder1/subfolder" (do not include source path, just the subpaths from
|
||||
the source path)
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
folderStartsWith:
|
||||
description: (global) exclude folders that start with these prefixes. Eg.
|
||||
"archive-" or "backup-"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
hidden:
|
||||
description: exclude hidden files and folders.
|
||||
type: boolean
|
||||
ignoreZeroSizeFolders:
|
||||
description: ignore folders with 0 size
|
||||
type: boolean
|
||||
type: object
|
||||
settings.ExternalLink:
|
||||
properties:
|
||||
text:
|
||||
@@ -302,21 +304,6 @@ definitions:
|
||||
styling:
|
||||
$ref: '#/definitions/settings.StylingConfig'
|
||||
type: object
|
||||
settings.IncludeIndexFilter:
|
||||
properties:
|
||||
rootFiles:
|
||||
description: list of root files to include, relative to the source path (eg.
|
||||
"file1.txt")
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
rootFolders:
|
||||
description: list of root folders to include, relative to the source path
|
||||
(eg. "folder1")
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
settings.Integrations:
|
||||
properties:
|
||||
media:
|
||||
@@ -582,6 +569,11 @@ definitions:
|
||||
type: object
|
||||
settings.SourceConfig:
|
||||
properties:
|
||||
conditionals:
|
||||
allOf:
|
||||
- $ref: '#/definitions/settings.ConditionalFilter'
|
||||
description: conditional rules to apply when indexing to include/exclude certain
|
||||
items
|
||||
createUserDir:
|
||||
description: create a user directory for each user
|
||||
type: boolean
|
||||
@@ -601,30 +593,9 @@ definitions:
|
||||
description: disable the source, this is useful so you don't need to remove
|
||||
it from the config file
|
||||
type: boolean
|
||||
exclude:
|
||||
allOf:
|
||||
- $ref: '#/definitions/settings.ExcludeIndexFilter'
|
||||
description: exclude files and folders from indexing, if include is not set
|
||||
include:
|
||||
allOf:
|
||||
- $ref: '#/definitions/settings.IncludeIndexFilter'
|
||||
description: include files and folders from indexing, if exclude is not set
|
||||
indexAlbumArt:
|
||||
description: 'deprecated: always enabled since 0.8.6'
|
||||
type: boolean
|
||||
indexingIntervalMinutes:
|
||||
description: optional manual overide interval in minutes to re-index the source
|
||||
type: integer
|
||||
maxWatchers:
|
||||
description: number of concurrent watchers to use for this source, currently
|
||||
not supported
|
||||
type: integer
|
||||
neverWatchPaths:
|
||||
description: paths that get initially once. Useful for folders that rarely
|
||||
change contents (without source path prefix)
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
private:
|
||||
description: designate as source as private -- currently just means no sharing
|
||||
permitted.
|
||||
@@ -751,8 +722,16 @@ definitions:
|
||||
type: object
|
||||
share.CreateBody:
|
||||
properties:
|
||||
allowUpload:
|
||||
description: AllowEdit bool `json:"allowEdit,omitempty"`
|
||||
allowCreate:
|
||||
description: allow creating files
|
||||
type: boolean
|
||||
allowDelete:
|
||||
type: boolean
|
||||
allowModify:
|
||||
description: allow modifying files
|
||||
type: boolean
|
||||
allowReplacements:
|
||||
description: allow replacements of files
|
||||
type: boolean
|
||||
allowedUsernames:
|
||||
items:
|
||||
@@ -764,7 +743,11 @@ definitions:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
type: boolean
|
||||
disableDownload:
|
||||
description: don't allow downloading files
|
||||
type: boolean
|
||||
disableFileViewer:
|
||||
description: don't allow viewing files
|
||||
type: boolean
|
||||
disableShareCard:
|
||||
type: boolean
|
||||
@@ -827,8 +810,16 @@ definitions:
|
||||
type: object
|
||||
share.Link:
|
||||
properties:
|
||||
allowUpload:
|
||||
description: AllowEdit bool `json:"allowEdit,omitempty"`
|
||||
allowCreate:
|
||||
description: allow creating files
|
||||
type: boolean
|
||||
allowDelete:
|
||||
type: boolean
|
||||
allowModify:
|
||||
description: allow modifying files
|
||||
type: boolean
|
||||
allowReplacements:
|
||||
description: allow replacements of files
|
||||
type: boolean
|
||||
allowedUsernames:
|
||||
items:
|
||||
@@ -840,7 +831,11 @@ definitions:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
type: boolean
|
||||
disableDownload:
|
||||
description: don't allow downloading files
|
||||
type: boolean
|
||||
disableFileViewer:
|
||||
description: don't allow viewing files
|
||||
type: boolean
|
||||
disableShareCard:
|
||||
type: boolean
|
||||
@@ -946,14 +941,28 @@ definitions:
|
||||
users.Permissions:
|
||||
properties:
|
||||
admin:
|
||||
description: allow admin access
|
||||
type: boolean
|
||||
api:
|
||||
description: allow api access
|
||||
type: boolean
|
||||
create:
|
||||
description: allow creating or uploading files
|
||||
type: boolean
|
||||
delete:
|
||||
description: allow deleting files
|
||||
type: boolean
|
||||
download:
|
||||
description: allow downloading files
|
||||
type: boolean
|
||||
modify:
|
||||
description: allow modifying files
|
||||
type: boolean
|
||||
realtime:
|
||||
description: allow realtime updates
|
||||
type: boolean
|
||||
share:
|
||||
description: allow sharing files
|
||||
type: boolean
|
||||
type: object
|
||||
users.Preview:
|
||||
@@ -1122,6 +1131,8 @@ definitions:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
version:
|
||||
type: integer
|
||||
viewMode:
|
||||
description: 'view mode to use: eg. normal, list, grid, or compact'
|
||||
type: string
|
||||
@@ -2771,6 +2782,63 @@ paths:
|
||||
summary: Upload files to a public upload share
|
||||
tags:
|
||||
- Public Shares
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Updates the content of a file in a public share.
|
||||
parameters:
|
||||
- description: Share hash for authentication
|
||||
in: query
|
||||
name: hash
|
||||
required: true
|
||||
type: string
|
||||
- description: Path to the file to update
|
||||
in: query
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: New content for the file
|
||||
in: body
|
||||
name: content
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: File updated successfully
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid request or parameters
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"403":
|
||||
description: Share unavailable or update not allowed
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"404":
|
||||
description: Share not found or file not found
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Update a file in a public share
|
||||
tags:
|
||||
- Public Shares
|
||||
/public/api/share:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
@@ -27,22 +27,21 @@ server:
|
||||
disabled: false # disable the source, this is useful so you don't need to remove it from the config file
|
||||
indexingIntervalMinutes: 0 # optional manual overide interval in minutes to re-index the source
|
||||
disableIndexing: false # disable the indexing of this source
|
||||
maxWatchers: 0 # number of concurrent watchers to use for this source, currently not supported
|
||||
neverWatchPaths: # paths that get initially once. Useful for folders that rarely change contents (without source path prefix)
|
||||
exclude: # exclude files and folders from indexing, if include is not set
|
||||
conditionals: # conditional rules to apply when indexing to include/exclude certain items
|
||||
hidden: false # exclude hidden files and folders.
|
||||
ignoreZeroSizeFolders: false # ignore folders with 0 size
|
||||
filePaths: # list of filepaths Eg. "folder1" or "file1.txt" or "folder1/file1.txt" (without source path prefix)
|
||||
folderPaths: # (filepath) list of folder names to include/exclude. Eg. "folder1" or "folder1/subfolder" (do not include source path, just the subpaths from the source path)
|
||||
fileNames: # (global) list of file names to include/exclude. Eg. "a.jpg"
|
||||
folderNames: # (global) list of folder names to include/exclude. Eg. "@eadir" or ".thumbnails"
|
||||
fileEndsWith: # (global) exclude files that end with these suffixes. Eg. ".jpg" or ".txt"
|
||||
folderEndsWith: # (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
|
||||
fileStartsWith: # (global) exclude files that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
folderStartsWith: # (global) exclude folders that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
include: # include files and folders from indexing, if exclude is not set
|
||||
rootFolders: # list of root folders to include, relative to the source path (eg. "folder1")
|
||||
rootFiles: # list of root files to include, relative to the source path (eg. "file1.txt")
|
||||
rules: # list of item rules to apply to specific paths
|
||||
- neverWatchPath: "" # index the folder in the first pass to get included in search, but never re-indexed.
|
||||
includeRootItem: "" # include only these items at root folder level
|
||||
fileStartsWith: "" # (global) exclude files that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
folderStartsWith: "" # (global) exclude folders that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
fileEndsWith: "" # (global) exclude files that end with these suffixes. Eg. ".jpg" or ".txt"
|
||||
folderEndsWith: "" # (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
|
||||
folderPath: "" # (global) exclude folders that match this path. Eg. "/path/to/folder" or "/path/to/folder/subfolder"
|
||||
filePath: "" # (global) exclude files that match this path. Eg. "/path/to/file.txt" or "/path/to/file.txt/subfile.txt"
|
||||
fileNames: "" # (global) exclude files that match these names. Eg. "file.txt" or "test.csv"
|
||||
folderNames: "" # (global) exclude folders that match these names. Eg. "folder" or "subfolder"
|
||||
viewable: false # Enable viewing in UI but exclude from indexing
|
||||
defaultUserScope: "/" # default "/" should match folders under path
|
||||
defaultEnabled: true # should be added as a default source for new users?
|
||||
createUserDir: false # create a user directory for each user
|
||||
@@ -143,11 +142,14 @@ userDefaults:
|
||||
defaultMediaPlayer: false # disable html5 media player and use the default media player
|
||||
folder: false # show thumbnail preview image for folder files
|
||||
permissions:
|
||||
api: false
|
||||
admin: false
|
||||
modify: false
|
||||
share: false
|
||||
realtime: false
|
||||
api: false # allow api access
|
||||
admin: false # allow admin access
|
||||
modify: false # allow modifying files
|
||||
share: false # allow sharing files
|
||||
realtime: false # allow realtime updates
|
||||
delete: false # allow deleting files
|
||||
create: false # allow creating or uploading files
|
||||
download: false # allow downloading files
|
||||
loginMethod: "password" # login method to use: eg. password, proxy, oidc
|
||||
disableUpdateNotifications: false # disable update notifications banner for admin users
|
||||
deleteWithoutConfirming: false # delete files without confirmation
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -9,7 +9,11 @@
|
||||
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="{{ .htmlVars.favicon }}">
|
||||
|
||||
<!--<link href="https://fonts.googleapis.com/css?family=Material+Icons|Material+Symbols+Outlined" rel="stylesheet">-->
|
||||
<!-- Preload critical fonts for optimal performance -->
|
||||
<link rel="preload" href="{{ .htmlVars.staticURL }}/fonts/roboto-normal-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="{{ .htmlVars.staticURL }}/fonts/roboto-medium-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="{{ .htmlVars.staticURL }}/fonts/roboto-bold-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="{{ .htmlVars.staticURL }}/fonts/material-icons.woff2" as="font" type="font/woff2" crossorigin>
|
||||
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
||||
@@ -26,10 +30,10 @@
|
||||
|
||||
<meta name="description" content="{{ .htmlVars.description }}">
|
||||
<link rel="canonical" href="https://github.com/gtsteffaniak/filebrowser">
|
||||
<link rel="canonical" href="https://github.com/gtsteffaniak">
|
||||
<link rel="canonical" href="https://github.com/quantumx-apps">
|
||||
<link rel="canonical" href="https://filebrowserquantum.com">
|
||||
|
||||
<!-- Inject Some Variables and generate the manifest json -->
|
||||
<!-- Injected variables come from backend server -->
|
||||
<script>
|
||||
window.globalVars = {{ .globalVars }};
|
||||
window.shareVars = {{ .shareVars }};
|
||||
|
||||
@@ -225,7 +225,6 @@ async function syncAllTranslations() {
|
||||
if (fileExisted) {
|
||||
try {
|
||||
targetContent = await fs.readJson(targetFile);
|
||||
console.log(`\nProcessing target language: ${targetLangCode} (from ${targetFile})`);
|
||||
} catch (e) {
|
||||
console.warn(`Warning: Could not parse ${targetFile}. Starting fresh. Error: ${e.message}`);
|
||||
targetContent = {};
|
||||
@@ -264,7 +263,7 @@ async function syncAllTranslations() {
|
||||
|
||||
if (checkOnly) {
|
||||
if (hasMeaningfulChanges) {
|
||||
console.log(`\n⚠️ Found ${meaningfulChanges} meaningful translation changes needed.`);
|
||||
console.log(`\n⚠️ Found ${meaningfulChanges} meaningful translation changes needed. -- update via "make sync-translations"`);
|
||||
return 1; // Exit code 1 for meaningful changes needed
|
||||
} else {
|
||||
console.log('\n✅ No meaningful translation changes needed.');
|
||||
|
||||
@@ -37,8 +37,24 @@ async function resourceAction(source, path, method, content) {
|
||||
if (content) {
|
||||
opts.body = content
|
||||
}
|
||||
const res = await fetchURL(apiPath, opts)
|
||||
return res
|
||||
|
||||
const response = await fetchURL(apiPath, opts)
|
||||
if (!response.ok) {
|
||||
const error = new Error(response.statusText);
|
||||
// attempt to marshal json response
|
||||
let data = null;
|
||||
try {
|
||||
data = await response.json()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
if (data) {
|
||||
error.message = data.message;
|
||||
}
|
||||
(/** @type {any} */ (error)).status = response.status;
|
||||
throw error;
|
||||
}
|
||||
return response
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Error performing resource action')
|
||||
throw err
|
||||
|
||||
+109
-2
@@ -1,8 +1,8 @@
|
||||
import { adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getApiPath, getPublicApiPath, encodedPath, doubleEncode } from "@/utils/url.js";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
import { state } from "@/store";
|
||||
import { globalVars,shareInfo } from "@/utils/constants";
|
||||
import { state, mutations } from "@/store";
|
||||
|
||||
// ============================================================================
|
||||
// PUBLIC API ENDPOINTS (hash-based authentication)
|
||||
@@ -177,4 +177,111 @@ export function post(
|
||||
// We are returning a promise, so we should return a rejected promise on error.
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function resourceAction(path, method, content) {
|
||||
if (!shareInfo.isValid) {
|
||||
throw new Error('invalid share')
|
||||
}
|
||||
try {
|
||||
path = doubleEncode(path)
|
||||
const apiPath = getPublicApiPath('resources', { path, hash: shareInfo.hash, token: shareInfo.token })
|
||||
const response = await fetch(apiPath, {
|
||||
method,
|
||||
body: content,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = new Error(response.statusText);
|
||||
// attempt to marshal json response
|
||||
let data = null;
|
||||
try {
|
||||
data = await response.json()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
if (data) {
|
||||
error.message = data.message;
|
||||
}
|
||||
(/** @type {any} */ (error)).status = response.status;
|
||||
throw error;
|
||||
}
|
||||
return response;
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Error performing resource action')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(path) {
|
||||
if (!shareInfo.isValid) {
|
||||
throw new Error('invalid share')
|
||||
}
|
||||
try {
|
||||
return await resourceAction(path, 'DELETE')
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Error deleting resource')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function put(path, content = '') {
|
||||
if (!shareInfo.isValid) {
|
||||
throw new Error('invalid share')
|
||||
}
|
||||
try {
|
||||
return await resourceAction(path, 'PUT', content)
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Error putting resource')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveCopy(
|
||||
items,
|
||||
action = 'copy',
|
||||
overwrite = false,
|
||||
rename = false
|
||||
) {
|
||||
let params = {
|
||||
overwrite: overwrite,
|
||||
action: action,
|
||||
rename: rename,
|
||||
hash: shareInfo.hash
|
||||
}
|
||||
try {
|
||||
// Create an array of fetch calls
|
||||
let promises = items.map(item => {
|
||||
let localParams = {
|
||||
...params,
|
||||
destination: doubleEncode(item.to),
|
||||
from: doubleEncode(item.from)
|
||||
}
|
||||
const apiPath = getPublicApiPath('resources', localParams)
|
||||
|
||||
return fetch(apiPath, { method: 'PATCH' }).then(response => {
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(
|
||||
`Failed to move/copy: ${text || response.statusText}`
|
||||
)
|
||||
})
|
||||
}
|
||||
return response
|
||||
})
|
||||
})
|
||||
|
||||
// Await all promises and ensure errors propagate
|
||||
await Promise.all(promises)
|
||||
setTimeout(() => {
|
||||
notify.showSuccess(
|
||||
action === 'copy' ? 'Resources copied successfully' : 'Resources moved successfully'
|
||||
)
|
||||
}, 125);
|
||||
setTimeout(() => {
|
||||
mutations.setReload(true);
|
||||
}, 125);
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Error moving/copying resources')
|
||||
throw err // Re-throw the error to propagate it back to the caller
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { state } from "@/store";
|
||||
import { renew, logout } from "@/utils/auth";
|
||||
import { renew } from "@/utils/auth";
|
||||
import { notify } from "@/notify";
|
||||
import { getters } from "@/store/getters";
|
||||
|
||||
export async function fetchURL(url, opts, auth = true) {
|
||||
opts = opts || {};
|
||||
@@ -34,11 +33,6 @@ export async function fetchURL(url, opts, auth = true) {
|
||||
if (res.status < 200 || res.status > 299) {
|
||||
let error = new Error(await res.text());
|
||||
error.status = res.status;
|
||||
|
||||
if (auth && res.status == 401 && !getters.isShare()) {
|
||||
logout();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -17,7 +17,7 @@
|
||||
<div class="context-menu-header">
|
||||
<div
|
||||
class="action button clickable"
|
||||
v-if="!isSearchActive && userPerms.modify && !isShare"
|
||||
v-if="!isSearchActive && permissions.create && !isShare"
|
||||
@click="toggleShowCreate"
|
||||
>
|
||||
<i v-if="!showCreate" class="material-icons">add</i>
|
||||
@@ -34,19 +34,19 @@
|
||||
</div>
|
||||
<hr class="divider">
|
||||
<action
|
||||
v-if="showCreate && !isSearchActive && userPerms.modify"
|
||||
v-if="showCreate && !isSearchActive"
|
||||
icon="create_new_folder"
|
||||
:label="$t('sidebar.newFolder')"
|
||||
@action="showHover('newDir')"
|
||||
/>
|
||||
<action
|
||||
v-if="showCreate && userPerms.modify && !isSearchActive"
|
||||
v-if="showCreate && !isSearchActive"
|
||||
icon="note_add"
|
||||
:label="$t('sidebar.newFile')"
|
||||
@action="showHover('newFile')"
|
||||
/>
|
||||
<action
|
||||
v-if="showCreate && userPerms.modify && !isSearchActive"
|
||||
v-if="showCreate && !isSearchActive"
|
||||
icon="file_upload"
|
||||
:label="$t('buttons.upload')"
|
||||
@action="uploadFunc"
|
||||
@@ -59,26 +59,26 @@
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="(!showCreate && selectedCount > 0)"
|
||||
v-if="(!showCreate && permissions.download && selectedCount > 0)"
|
||||
icon="file_download"
|
||||
:label="$t('buttons.download')"
|
||||
@action="startDownload"
|
||||
:counter="selectedCount"
|
||||
/>
|
||||
<action
|
||||
v-if="selectedCount <= 1 && showShare"
|
||||
v-if="(showCreate || selectedCount == 1) && showShare"
|
||||
icon="share"
|
||||
:label="$t('buttons.share')"
|
||||
@action="showShareHover"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount == 1 && userPerms.modify && !isSearchActive"
|
||||
v-if="!showCreate && selectedCount == 1 && permissions.modify && !isSearchActive"
|
||||
icon="mode_edit"
|
||||
:label="$t('buttons.rename')"
|
||||
@action="showRenameHover"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
v-if="!showCreate && selectedCount > 0 && permissions.modify"
|
||||
icon="content_copy"
|
||||
:label="$t('buttons.copyFile')"
|
||||
show="copy"
|
||||
@@ -90,11 +90,17 @@
|
||||
@action="openParentFolder"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
v-if="!showCreate && selectedCount > 0 && permissions.modify"
|
||||
icon="forward"
|
||||
:label="$t('buttons.moveFile')"
|
||||
show="move"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && !isSearchActive && req?.items?.length > 0"
|
||||
icon="select_all"
|
||||
:label="$t('buttons.selectAll')"
|
||||
@action="selectAllItems"
|
||||
/>
|
||||
<action
|
||||
v-if="showDelete"
|
||||
icon="delete"
|
||||
@@ -173,6 +179,12 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
permissions() {
|
||||
return getters.permissions();
|
||||
},
|
||||
req() {
|
||||
return state.req;
|
||||
},
|
||||
isShare() {
|
||||
return getters.isShare();
|
||||
},
|
||||
@@ -192,28 +204,26 @@ export default {
|
||||
return this.showEdit || this.showDelete || this.showSave || this.showGoToRaw || this.hasDownload;
|
||||
},
|
||||
showGoToRaw() {
|
||||
if (!this.permissions.download) {
|
||||
return false;
|
||||
}
|
||||
const cv = getters.currentView();
|
||||
return cv == "preview" || cv == "markdownViewer" || cv == "editor";
|
||||
},
|
||||
showEdit() {
|
||||
const cv = getters.currentView();
|
||||
if (getters.isShare()) {
|
||||
// TODO: add support for editing shared files
|
||||
return false;
|
||||
}
|
||||
return cv == "markdownViewer" && state.user?.permissions?.modify;
|
||||
return cv == "markdownViewer" && this.permissions.modify;
|
||||
},
|
||||
showDelete() {
|
||||
const cv = getters.currentView();
|
||||
if (getters.isShare() || !state.user?.permissions?.modify) {
|
||||
// TODO: add support for deleting shared files
|
||||
if (this.selectedCount == 0) {
|
||||
return false;
|
||||
}
|
||||
const showDelete = cv != "settings" && !this.showCreate && this.selectedCount > 0 && !this.isSearchActive;
|
||||
const cv = getters.currentView();
|
||||
const showDelete = cv != "settings" && !this.isSearchActive && this.permissions.delete;
|
||||
return showDelete;
|
||||
},
|
||||
hasDownload() {
|
||||
return this.selectedCount > 0;
|
||||
return this.selectedCount > 0 && this.permissions.download;
|
||||
},
|
||||
isPreview() {
|
||||
const cv = getters.currentView();
|
||||
@@ -231,19 +241,22 @@ export default {
|
||||
// TODO: add support for saving shared files
|
||||
return false;
|
||||
}
|
||||
return getters.currentView() == "editor" && state.user?.permissions?.modify;
|
||||
return getters.currentView() == "editor" && this.permissions.modify;
|
||||
},
|
||||
showOverflow() {
|
||||
return getters.currentPromptName() == "OverflowMenu";
|
||||
},
|
||||
showAccess() {
|
||||
return state.user?.permissions?.admin && this.showCreate;
|
||||
if (getters.isShare()) {
|
||||
return false;
|
||||
}
|
||||
return this.permissions.admin && this.showCreate;
|
||||
},
|
||||
showShare() {
|
||||
if (getters.isShare()) {
|
||||
return false;
|
||||
}
|
||||
return state.user?.permissions?.share;
|
||||
return this.permissions.share;
|
||||
},
|
||||
showContext() {
|
||||
return getters.currentPromptName() == "ContextMenu";
|
||||
@@ -272,13 +285,6 @@ export default {
|
||||
selectedCount() {
|
||||
return getters.selectedCount();
|
||||
},
|
||||
userPerms() {
|
||||
return {
|
||||
upload: state.user?.permissions?.modify && state.selected.length > 0,
|
||||
share: state.user?.permissions?.share,
|
||||
modify: state.user?.permissions?.modify,
|
||||
};
|
||||
},
|
||||
currentPrompt() {
|
||||
return getters.currentPrompt();
|
||||
},
|
||||
@@ -322,6 +328,10 @@ export default {
|
||||
});
|
||||
},
|
||||
toggleShowCreate() {
|
||||
if (!this.permissions.create) {
|
||||
this.showCreate = false;
|
||||
return;
|
||||
}
|
||||
this.showCreate = !this.showCreate;
|
||||
},
|
||||
shouldShowParentFolder() {
|
||||
@@ -399,7 +409,7 @@ export default {
|
||||
}, 300);
|
||||
},
|
||||
startShowCreate() {
|
||||
if (getters.isShare()) {
|
||||
if (!this.permissions.create) {
|
||||
return;
|
||||
}
|
||||
this.showCreate = true;
|
||||
@@ -432,7 +442,7 @@ export default {
|
||||
},
|
||||
initializeCreateState() {
|
||||
// Only set initial showCreate state, don't override user choices
|
||||
if (state.selected.length > 0 || getters.isShare()) {
|
||||
if (state.selected.length > 0 || !this.permissions.create) {
|
||||
this.showCreate = false;
|
||||
} else {
|
||||
this.showCreate = true;
|
||||
@@ -493,7 +503,19 @@ export default {
|
||||
if (parentPath == "") {
|
||||
parentPath = "/";
|
||||
}
|
||||
url.goToItem(state.req.source, parentPath);
|
||||
url.goToItem(state.req.source, parentPath, {});
|
||||
},
|
||||
selectAllItems() {
|
||||
if (state.req && state.req.items && state.req.items.length > 0) {
|
||||
// Clear current selection first
|
||||
mutations.resetSelected();
|
||||
// Add all items from current directory to selection by their indices
|
||||
state.req.items.forEach((item, index) => {
|
||||
mutations.addSelected(index);
|
||||
});
|
||||
// Close the context menu
|
||||
mutations.closeHovers();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -249,6 +249,16 @@ export default {
|
||||
// For other browsers, use regular contextmenu
|
||||
this.$el.addEventListener("contextmenu", this.openContext);
|
||||
}
|
||||
|
||||
// Add keyboard event listener for "/" to activate search
|
||||
this.handleKeydown = (event) => {
|
||||
if (event.key === '/' && !state.isSearchActive) {
|
||||
event.preventDefault();
|
||||
this.open();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', this.handleKeydown);
|
||||
},
|
||||
beforeUnmount() {
|
||||
// If Safari, remove touchstart listener
|
||||
@@ -261,6 +271,11 @@ export default {
|
||||
} else {
|
||||
this.$el.removeEventListener("contextmenu", this.openContext);
|
||||
}
|
||||
|
||||
// Clean up keyboard event listener
|
||||
if (this.handleKeydown) {
|
||||
document.removeEventListener('keydown', this.handleKeydown);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
disableSearchOptions() {
|
||||
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
relative: { x: 0, y: 0 },
|
||||
},
|
||||
maxScale: 4,
|
||||
minScale: 0.25,
|
||||
minScale: 1, // Minimum scale is 1 (full frame view)
|
||||
isTiff: false, // Determine if the image is a TIFF
|
||||
// Swipe navigation properties
|
||||
swipeStartTime: null,
|
||||
@@ -178,8 +178,8 @@ export default {
|
||||
this.lastY = null;
|
||||
this.lastTouchDistance = null;
|
||||
|
||||
// Initialize swipe tracking for single touch
|
||||
if (event.targetTouches.length === 1) {
|
||||
// Initialize swipe tracking for single touch only when at full frame view (scale === 1)
|
||||
if (event.targetTouches.length === 1 && this.scale === 1) {
|
||||
const touch = event.targetTouches[0];
|
||||
this.swipeStartTime = Date.now();
|
||||
this.swipeStartX = touch.pageX;
|
||||
@@ -191,7 +191,7 @@ export default {
|
||||
this.gestureDecided = false; // Reset decision for new touch
|
||||
|
||||
} else {
|
||||
// Reset swipe tracking for multi-touch (zoom gestures)
|
||||
// Reset swipe tracking for multi-touch (zoom gestures) or when zoomed in
|
||||
this.resetSwipeTracking();
|
||||
}
|
||||
|
||||
@@ -202,9 +202,15 @@ export default {
|
||||
this.touches++;
|
||||
if (this.touches > 1) {
|
||||
this.zoomAuto(event);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
// Only prevent default if zoomed in (need to pan) or multi-touch (need to zoom)
|
||||
// This allows nav-zone touches to work when at full frame view
|
||||
if (this.scale > 1 || event.targetTouches.length >= 2) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
zoomAuto(event) {
|
||||
switch (this.scale) {
|
||||
@@ -224,10 +230,8 @@ export default {
|
||||
event.preventDefault();
|
||||
},
|
||||
touchMove(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Update current swipe position for single touch
|
||||
if (event.targetTouches.length === 1) {
|
||||
// Update current swipe position for single touch, only when at full frame view
|
||||
if (event.targetTouches.length === 1 && this.scale === 1) {
|
||||
const touch = event.targetTouches[0];
|
||||
this.swipeCurrentX = touch.pageX;
|
||||
this.swipeCurrentY = touch.pageY;
|
||||
@@ -244,6 +248,7 @@ export default {
|
||||
if (deltaX > deltaY * 2) {
|
||||
// Horizontal movement is significantly more than vertical - it's a swipe
|
||||
this.isSwipeGesture = true;
|
||||
event.preventDefault(); // Prevent scrolling during swipe
|
||||
} else {
|
||||
// Not horizontal enough - it's a pan gesture
|
||||
this.isSwipeGesture = false;
|
||||
@@ -251,12 +256,18 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// If we've decided it's a swipe gesture, don't do normal panning
|
||||
// If we've decided it's a swipe gesture, prevent default and don't do normal panning
|
||||
if (this.gestureDecided && this.isSwipeGesture) {
|
||||
event.preventDefault();
|
||||
return; // Block normal pan behavior for swipes
|
||||
}
|
||||
}
|
||||
|
||||
// Only prevent default if we're zoomed in or doing multi-touch zoom
|
||||
if (this.scale > 1 || event.targetTouches.length >= 2) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Normal touch move logic for pan/zoom (only runs if not a swipe gesture)
|
||||
if (this.lastX === null) {
|
||||
this.lastX = event.targetTouches[0].pageX;
|
||||
@@ -284,7 +295,8 @@ export default {
|
||||
this.scale += (touchDistance - this.lastTouchDistance) / step;
|
||||
this.lastTouchDistance = touchDistance;
|
||||
this.setZoom();
|
||||
} else if (event.targetTouches.length === 1) {
|
||||
} else if (event.targetTouches.length === 1 && this.scale > 1) {
|
||||
// Only allow panning when zoomed in
|
||||
if (this.moveDisabled) return;
|
||||
let x = event.targetTouches[0].pageX - this.lastX;
|
||||
let y = event.targetTouches[0].pageY - this.lastY;
|
||||
@@ -308,6 +320,11 @@ export default {
|
||||
setZoom() {
|
||||
this.scale = Math.max(this.minScale, Math.min(this.maxScale, this.scale));
|
||||
|
||||
// If scale is back to 1 (full frame view), reset position to center
|
||||
if (this.scale === 1) {
|
||||
this.position.relative = { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
// Update the transform with both translate and scale values
|
||||
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
|
||||
},
|
||||
@@ -315,10 +332,10 @@ export default {
|
||||
return +style.replace("px", "");
|
||||
},
|
||||
touchEnd(event) {
|
||||
event.preventDefault();
|
||||
let handledSwipe = false;
|
||||
|
||||
// Only process swipe if it was a single touch and we detected a swipe gesture
|
||||
if (this.isSwipeGesture && this.swipeStartTime) {
|
||||
// Only process swipe if it was a single touch, we detected a swipe gesture, and at full frame view
|
||||
if (this.isSwipeGesture && this.swipeStartTime && this.scale === 1) {
|
||||
const swipeEndTime = Date.now();
|
||||
const swipeDuration = swipeEndTime - this.swipeStartTime;
|
||||
const deltaX = this.swipeCurrentX - this.swipeStartX;
|
||||
@@ -336,9 +353,16 @@ export default {
|
||||
} else {
|
||||
this.$emit('navigate-next');
|
||||
}
|
||||
handledSwipe = true;
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// Only prevent default if we handled a swipe or were zoomed in (panning)
|
||||
if (!handledSwipe && this.scale > 1) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Reset swipe tracking
|
||||
this.resetSwipeTracking();
|
||||
},
|
||||
|
||||
@@ -166,7 +166,7 @@ export default {
|
||||
return true;
|
||||
},
|
||||
thumbnailUrl() {
|
||||
if (!globalVars.enableThumbs) {
|
||||
if (!globalVars.enableThumbs || !state.req.path || !this.name) {
|
||||
return "";
|
||||
}
|
||||
const previewPath = url.joinPath(state.req.path, this.name);
|
||||
@@ -305,14 +305,17 @@ export default {
|
||||
},
|
||||
/** @param {DragEvent} event */
|
||||
async drop(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.isDraggedOver = false;
|
||||
|
||||
if (!this.canDrop) {
|
||||
// Don't prevent default or stop propagation - let the parent ListingView handle it
|
||||
return;
|
||||
}
|
||||
|
||||
// Only stop propagation if we're actually going to handle this drop (moving files into a folder)
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
let items = [];
|
||||
for (let i of state.selected) {
|
||||
items.push({
|
||||
@@ -326,11 +329,15 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
const conflict = upload.checkConflict(
|
||||
items,
|
||||
// @ts-ignore
|
||||
(await filesApi.fetchFiles(this.source, this.path)).items || []
|
||||
);
|
||||
let checkAction = async () => {
|
||||
if (getters.isShare()) {
|
||||
return await publicApi.fetchPub(this.path, shareInfo.hash);
|
||||
} else {
|
||||
return await filesApi.fetchFiles(this.source, this.path);
|
||||
}
|
||||
}
|
||||
const response = await checkAction();
|
||||
const conflict = upload.checkConflict(items, response?.items || [] );
|
||||
|
||||
/**
|
||||
* @param {boolean} overwrite
|
||||
@@ -346,7 +353,11 @@ export default {
|
||||
});
|
||||
|
||||
try {
|
||||
await filesApi.moveCopy(items, "move", overwrite, rename);
|
||||
if (getters.isShare()) {
|
||||
await publicApi.moveCopy(items, "move", overwrite, rename);
|
||||
} else {
|
||||
await filesApi.moveCopy(items, "move", overwrite, rename);
|
||||
}
|
||||
// Close the prompt after successful operation
|
||||
mutations.closeHovers();
|
||||
} catch (error) {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
</div>
|
||||
|
||||
<div class="share__box__element share__box__center">
|
||||
<div v-if="!shareInfo.disableDownload" class="share__box__element share__box__center">
|
||||
<button class="button button--flat clickable" @click="goToLink()"> {{ $t("buttons.download") }} </button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
@mousemove="toggleNavigation"
|
||||
@touchstart="(e) => { handleTouchStart(e); toggleNavigation(e); }"
|
||||
@touchmove="handleTouchMove"
|
||||
@click="handleClick"
|
||||
></div>
|
||||
|
||||
<!-- Right edge detection zone -->
|
||||
@@ -17,7 +16,6 @@
|
||||
@mousemove="toggleNavigation"
|
||||
@touchstart="(e) => { handleTouchStart(e); toggleNavigation(e); }"
|
||||
@touchmove="handleTouchMove"
|
||||
@click="handleClick"
|
||||
></div>
|
||||
|
||||
<!-- Previous button -->
|
||||
@@ -38,6 +36,7 @@
|
||||
dragging: dragState.type === 'previous',
|
||||
active: dragState.atFullExtent && dragState.type === 'previous',
|
||||
'dark-mode': isDarkMode,
|
||||
'media-mode': isMediaQueueMode,
|
||||
}"
|
||||
:style="dragState.type === 'previous' ? { transform: `translateY(-50%) translate(${dragState.deltaX}px, 0)` } : {}"
|
||||
:aria-label="$t('buttons.previous')"
|
||||
@@ -59,7 +58,7 @@
|
||||
@mouseover="setHoverNav(true)"
|
||||
@mouseleave="setHoverNav(false)"
|
||||
class="nav-button nav-next"
|
||||
:class="{ hidden: !showNav, dragging: dragState.type === 'next', active: dragState.atFullExtent && dragState.type === 'next','dark-mode': isDarkMode}"
|
||||
:class="{ hidden: !showNav, dragging: dragState.type === 'next', active: dragState.atFullExtent && dragState.type === 'next','dark-mode': isDarkMode, 'media-mode': isMediaQueueMode}"
|
||||
:style="dragState.type === 'next' ? { transform: `translateY(-50%) translate(${dragState.deltaX}px, 0)` } : {}"
|
||||
:aria-label="$t('buttons.next')"
|
||||
:title="$t('buttons.next')"
|
||||
@@ -119,19 +118,23 @@ export default {
|
||||
return getters.isSidebarVisible() && getters.isStickySidebar();
|
||||
},
|
||||
enabled() {
|
||||
return state.navigation.enabled;
|
||||
return state.navigation.enabled && getters.currentPrompt() == null;
|
||||
},
|
||||
showNav() {
|
||||
const shouldShow = state.navigation.show || this.hoverNav;
|
||||
return shouldShow;
|
||||
},
|
||||
hasPrevious() {
|
||||
const has = state.navigation.previousLink !== "";
|
||||
return has;
|
||||
if (this.isMediaQueueMode) {
|
||||
return this.hasMediaPrevious();
|
||||
}
|
||||
return state.navigation.previousLink !== "";
|
||||
},
|
||||
hasNext() {
|
||||
const has = state.navigation.nextLink !== "";
|
||||
return has;
|
||||
if (this.isMediaQueueMode) {
|
||||
return this.hasMediaNext();
|
||||
}
|
||||
return state.navigation.nextLink !== "";
|
||||
},
|
||||
previousRaw() {
|
||||
return state.navigation.previousRaw;
|
||||
@@ -142,6 +145,16 @@ export default {
|
||||
currentView() {
|
||||
const view = getters.currentView();
|
||||
return view;
|
||||
},
|
||||
isMediaQueueMode() {
|
||||
const previewType = getters.previewType();
|
||||
const isMediaView = previewType === 'audio' || previewType === 'video';
|
||||
const mode = state.playbackQueue?.mode || 'single';
|
||||
const queueLength = state.playbackQueue?.queue?.length || 0;
|
||||
const hasQueue = queueLength > 1;
|
||||
|
||||
// Use media queue when in media view, NOT in single/loop-single mode, and have a queue
|
||||
return isMediaView && mode !== 'single' && mode !== 'loop-single' && hasQueue;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -199,6 +212,7 @@ export default {
|
||||
window.addEventListener("mouseup", this.endDrag);
|
||||
window.addEventListener("touchmove", this.handleDrag, { passive: false });
|
||||
window.addEventListener("touchend", this.endDrag);
|
||||
document.addEventListener("click", this.handleDocumentClick);
|
||||
|
||||
// Calculate 10em threshold in pixels
|
||||
const emSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
@@ -223,6 +237,7 @@ export default {
|
||||
window.removeEventListener("mouseup", this.endDrag);
|
||||
window.removeEventListener("touchmove", this.handleDrag);
|
||||
window.removeEventListener("touchend", this.endDrag);
|
||||
document.removeEventListener("click", this.handleDocumentClick);
|
||||
|
||||
// Clear our local timeout
|
||||
if (this.navigationTimeout) {
|
||||
@@ -247,14 +262,31 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const directoryPath = url.removeLastDir(state.req.path);
|
||||
let directoryPath = url.removeLastDir(state.req.path);
|
||||
|
||||
// If directoryPath is empty, the file is in root - use '/' as the directory
|
||||
if (!directoryPath || directoryPath === '') {
|
||||
directoryPath = '/';
|
||||
}
|
||||
|
||||
// Special case: if we're viewing a shared single file (where the share itself is the file)
|
||||
// and directoryPath equals req.path, there's no directory to navigate within
|
||||
if (getters.isShare() && directoryPath === state.req.path) {
|
||||
// This is a single file share with no siblings to navigate to
|
||||
mutations.clearNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
let listing = null;
|
||||
|
||||
// Try to get listing from current request first
|
||||
if (state.req.items) {
|
||||
listing = state.req.items;
|
||||
} else {
|
||||
// Fetch the directory listing
|
||||
} else if (state.req.parentDirItems) {
|
||||
// Use pre-fetched parent directory items from Files.vue
|
||||
listing = state.req.parentDirItems;
|
||||
} else if (directoryPath !== state.req.path) {
|
||||
// Fetch directory listing (now with '/' for root files)
|
||||
try {
|
||||
let res;
|
||||
if (getters.isShare()) {
|
||||
@@ -264,8 +296,15 @@ export default {
|
||||
}
|
||||
listing = res.items;
|
||||
} catch (error) {
|
||||
listing = [state.req]; // Fallback to current item only
|
||||
// If we can't fetch the directory listing, navigation isn't possible
|
||||
mutations.clearNavigation();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// This is a file at root where directoryPath === req.path
|
||||
// This shouldn't normally happen for non-share cases, but handle gracefully
|
||||
mutations.clearNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
mutations.setupNavigation({
|
||||
@@ -296,15 +335,141 @@ export default {
|
||||
prev() {
|
||||
if (this.hasPrevious) {
|
||||
this.hoverNav = false;
|
||||
this.$router.replace({ path: state.navigation.previousLink });
|
||||
|
||||
// Set transitioning state - keeps old req visible until new one loads
|
||||
// Editor and other components check isTransitioning to prevent saves
|
||||
mutations.setNavigationTransitioning(true);
|
||||
|
||||
if (this.isMediaQueueMode) {
|
||||
this.navigateMediaPrevious();
|
||||
} else {
|
||||
this.$router.replace({ path: state.navigation.previousLink });
|
||||
}
|
||||
}
|
||||
},
|
||||
next() {
|
||||
if (this.hasNext) {
|
||||
this.hoverNav = false;
|
||||
this.$router.replace({ path: state.navigation.nextLink });
|
||||
|
||||
// Set transitioning state - keeps old req visible until new one loads
|
||||
// Editor and other components check isTransitioning to prevent saves
|
||||
mutations.setNavigationTransitioning(true);
|
||||
|
||||
if (this.isMediaQueueMode) {
|
||||
this.navigateMediaNext();
|
||||
} else {
|
||||
this.$router.replace({ path: state.navigation.nextLink });
|
||||
}
|
||||
}
|
||||
},
|
||||
hasMediaPrevious() {
|
||||
const queue = state.playbackQueue?.queue || [];
|
||||
const currentIndex = state.playbackQueue?.currentIndex ?? -1;
|
||||
const mode = state.playbackQueue?.mode || 'single';
|
||||
|
||||
if (queue.length <= 1 || currentIndex < 0) return false;
|
||||
|
||||
// For sequential mode, no previous if at start
|
||||
if (mode === 'sequential' && currentIndex === 0) return false;
|
||||
|
||||
// For loop-all and shuffle, always have previous (wraps around)
|
||||
return true;
|
||||
},
|
||||
hasMediaNext() {
|
||||
const queue = state.playbackQueue?.queue || [];
|
||||
const currentIndex = state.playbackQueue?.currentIndex ?? -1;
|
||||
const mode = state.playbackQueue?.mode || 'single';
|
||||
|
||||
if (queue.length <= 1 || currentIndex < 0) return false;
|
||||
|
||||
// For sequential mode, no next if at end
|
||||
if (mode === 'sequential' && currentIndex >= queue.length - 1) return false;
|
||||
|
||||
// For loop-all and shuffle, always have next (wraps around)
|
||||
return true;
|
||||
},
|
||||
navigateMediaPrevious() {
|
||||
const queue = state.playbackQueue?.queue || [];
|
||||
const currentIndex = state.playbackQueue?.currentIndex ?? -1;
|
||||
const mode = state.playbackQueue?.mode || 'single';
|
||||
|
||||
if (queue.length === 0 || currentIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let prevIndex = currentIndex - 1;
|
||||
|
||||
// Handle wrapping
|
||||
if (prevIndex < 0) {
|
||||
if (mode === 'loop-all' || mode === 'shuffle') {
|
||||
prevIndex = queue.length - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const prevItem = queue[prevIndex];
|
||||
if (!prevItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update queue index
|
||||
mutations.setPlaybackQueue({
|
||||
queue: queue,
|
||||
currentIndex: prevIndex,
|
||||
mode: mode
|
||||
});
|
||||
|
||||
// Navigate
|
||||
const prevItemUrl = url.buildItemUrl(prevItem.source || state.req.source, prevItem.path);
|
||||
mutations.replaceRequest(prevItem);
|
||||
this.$router.replace({ path: prevItemUrl }).catch(err => {
|
||||
if (err.name !== 'NavigationDuplicated') {
|
||||
// Silently ignore navigation errors
|
||||
}
|
||||
});
|
||||
},
|
||||
navigateMediaNext() {
|
||||
const queue = state.playbackQueue?.queue || [];
|
||||
const currentIndex = state.playbackQueue?.currentIndex ?? -1;
|
||||
const mode = state.playbackQueue?.mode || 'single';
|
||||
|
||||
if (queue.length === 0 || currentIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextIndex = currentIndex + 1;
|
||||
|
||||
// Handle wrapping
|
||||
if (nextIndex >= queue.length) {
|
||||
if (mode === 'loop-all' || mode === 'shuffle') {
|
||||
nextIndex = 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const nextItem = queue[nextIndex];
|
||||
if (!nextItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update queue index
|
||||
mutations.setPlaybackQueue({
|
||||
queue: queue,
|
||||
currentIndex: nextIndex,
|
||||
mode: mode
|
||||
});
|
||||
|
||||
// Navigate
|
||||
const nextItemUrl = url.buildItemUrl(nextItem.source || state.req.source, nextItem.path);
|
||||
mutations.replaceRequest(nextItem);
|
||||
this.$router.replace({ path: nextItemUrl }).catch(err => {
|
||||
if (err.name !== 'NavigationDuplicated') {
|
||||
// Silently ignore navigation errors
|
||||
}
|
||||
});
|
||||
},
|
||||
keyEvent(event) {
|
||||
// Only handle navigation if enabled and no prompt is active
|
||||
if (!this.enabled || state.prompts.length > 0) {
|
||||
@@ -363,6 +528,41 @@ export default {
|
||||
|
||||
this.showNavigation();
|
||||
},
|
||||
handleDocumentClick(event) {
|
||||
// Only handle clicks if navigation is enabled
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show navigation if this is part of a swipe gesture
|
||||
if (this.isSwipe) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if click is in the left edge zone
|
||||
if (this.hasPrevious && this.isClickInLeftZone(event)) {
|
||||
this.showNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if click is in the right edge zone
|
||||
if (this.hasNext && this.isClickInRightZone(event)) {
|
||||
this.showNavigation();
|
||||
return;
|
||||
}
|
||||
},
|
||||
isClickInLeftZone(event) {
|
||||
const zoneWidth = 3 * parseFloat(getComputedStyle(document.documentElement).fontSize); // 3em in pixels
|
||||
const sidebarOffset = this.moveWithSidebar ? 20 * parseFloat(getComputedStyle(document.documentElement).fontSize) : 0; // 20em in pixels
|
||||
|
||||
return event.clientX >= sidebarOffset && event.clientX <= (sidebarOffset + zoneWidth);
|
||||
},
|
||||
isClickInRightZone(event) {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const zoneWidth = 3 * parseFloat(getComputedStyle(document.documentElement).fontSize); // 3em in pixels
|
||||
|
||||
return event.clientX >= (viewportWidth - zoneWidth) && event.clientX <= viewportWidth;
|
||||
},
|
||||
showNavigation() {
|
||||
mutations.setNavigationShow(true);
|
||||
mutations.clearNavigationTimeout();
|
||||
@@ -758,9 +958,9 @@ export default {
|
||||
position: fixed;
|
||||
top: 25%; /* Start at 25% from top */
|
||||
bottom: 25%; /* End at 25% from bottom (so middle 50%) */
|
||||
width: 3em; /* Reduced width to minimize content interference */
|
||||
pointer-events: auto;
|
||||
z-index: 5; /* Lower z-index so content can appear above */
|
||||
width: 5em;
|
||||
pointer-events: none; /* Allow clicks to pass through to content behind */
|
||||
z-index: -1; /* Negative z-index to ensure content appears above */
|
||||
background: transparent; /* Invisible zones for mouse/touch detection */
|
||||
}
|
||||
|
||||
@@ -802,6 +1002,10 @@ export default {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.nav-button.media-mode {
|
||||
color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.nav-button:hover,
|
||||
.nav-button.active {
|
||||
background: var(--primaryColor);
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.copyPasteConfirm") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ operation === 'copy' ? $t("prompts.copyItemQuestion") : $t("prompts.moveItemQuestion") }}</p>
|
||||
|
||||
<div class="path-info">
|
||||
<div class="path-section">
|
||||
<strong>{{ $t("prompts.sourcePath") }}</strong>
|
||||
<div class="path-display">
|
||||
<span class="path-item" v-for="(item, index) in sourceItems" :key="index">
|
||||
{{ item }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="path-section">
|
||||
<strong>{{ $t("prompts.destinationPath") }}</strong>
|
||||
<div class="path-display">
|
||||
<span class="path-item" v-for="(item, index) in destinationItems" :key="index">
|
||||
{{ item }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
@click="closeHovers"
|
||||
class="button button--flat button--grey"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
@click="confirm"
|
||||
class="button button--flat"
|
||||
:aria-label="operation === 'copy' ? $t('buttons.copy') : $t('buttons.move')"
|
||||
:title="operation === 'copy' ? $t('buttons.copy') : $t('buttons.move')">
|
||||
{{ operation === 'copy' ? $t('buttons.copy') : $t('buttons.move') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mutations } from "@/store";
|
||||
|
||||
export default {
|
||||
name: "copy-paste-confirm",
|
||||
props: {
|
||||
operation: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value) => ["copy", "move"].includes(value),
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
onConfirm: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sourceItems() {
|
||||
return this.items.map(item => {
|
||||
const source = item.fromSource || '';
|
||||
const path = item.from || '';
|
||||
return source ? `${source}${path}` : path;
|
||||
});
|
||||
},
|
||||
destinationItems() {
|
||||
return this.items.map(item => {
|
||||
const source = item.toSource || '';
|
||||
const path = item.to || '';
|
||||
return source ? `${source}${path}` : path;
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeHovers() {
|
||||
mutations.closeHovers();
|
||||
},
|
||||
confirm() {
|
||||
this.onConfirm();
|
||||
mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.path-info {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.path-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.path-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
padding: 0.75em;
|
||||
background-color: var(--surfaceSecondary);
|
||||
border-radius: 4px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.path-item {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
word-break: break-all;
|
||||
padding: 0.25em 0;
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { filesApi } from "@/api";
|
||||
import { filesApi, publicApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { notify } from "@/notify";
|
||||
@@ -70,7 +70,11 @@ export default {
|
||||
try {
|
||||
const currentView = getters.currentView()
|
||||
if (state.isSearchActive || currentView == "preview") {
|
||||
await filesApi.remove(state.selected[0].source, state.selected[0].path);
|
||||
if (getters.isShare()) {
|
||||
await publicApi.remove(state.selected[0].path);
|
||||
} else {
|
||||
await filesApi.remove(state.selected[0].source, state.selected[0].path);
|
||||
}
|
||||
buttons.success("delete");
|
||||
notify.showSuccess("Deleted item successfully");
|
||||
mutations.closeHovers();
|
||||
@@ -79,11 +83,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
if (!this.isListing) {
|
||||
await filesApi.remove(state.req.items.source, state.req.items[state.selected[0]].path);
|
||||
if (getters.isShare()) {
|
||||
await publicApi.remove(state.req.items[state.selected[0]].path);
|
||||
} else {
|
||||
await filesApi.remove(state.req.items.source, state.req.items[state.selected[0]].path);
|
||||
}
|
||||
buttons.success("delete");
|
||||
notify.showSuccess("Deleted item successfully");
|
||||
|
||||
this.currentPrompt?.confirm();
|
||||
mutations.closeHovers();
|
||||
return;
|
||||
}
|
||||
@@ -96,7 +102,11 @@ export default {
|
||||
|
||||
let promises = [];
|
||||
for (let index of state.selected) {
|
||||
promises.push(filesApi.remove(state.req.source, state.req.items[index].path));
|
||||
if (getters.isShare()) {
|
||||
promises.push(publicApi.remove(state.req.items[index].path));
|
||||
} else {
|
||||
promises.push(filesApi.remove(state.req.source, state.req.items[index].path));
|
||||
}
|
||||
}
|
||||
mutations.resetSelected();
|
||||
await Promise.all(promises);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<!-- Source Selection Dropdown (only show if multiple sources available and not in display mode) -->
|
||||
<div v-if="availableSources.length > 1 && !isDisplayMode" class="source-selector" style="margin-bottom: 1rem;">
|
||||
<div v-if="showSourceSelector" class="source-selector" style="margin-bottom: 1rem;">
|
||||
<label for="destinationSource" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
{{ $t("prompts.destinationSource") }}
|
||||
</label>
|
||||
@@ -42,10 +42,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, mutations } from "@/store";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { url } from "@/utils";
|
||||
import { filesApi } from "@/api";
|
||||
import { filesApi, publicApi } from "@/api";
|
||||
import Icon from "@/components/files/Icon.vue";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "file-list",
|
||||
@@ -93,6 +94,9 @@ export default {
|
||||
return this.title || this.$t("general.files");
|
||||
},
|
||||
sourcePath() {
|
||||
if (getters.isShare()) {
|
||||
return { source: this.source, path: this.path };
|
||||
}
|
||||
return { source: this.source, path: this.path };
|
||||
},
|
||||
effectiveSource() {
|
||||
@@ -106,6 +110,9 @@ export default {
|
||||
// Display mode when fileList prop is provided (drag-triggered navigation)
|
||||
return this.fileList !== null;
|
||||
},
|
||||
showSourceSelector() {
|
||||
return this.availableSources.length > 1 && !this.isDisplayMode && !getters.isShare();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
browseSource(newSource) {
|
||||
@@ -199,8 +206,13 @@ export default {
|
||||
let clickedItem = this.items.find(item => item.path === path);
|
||||
let sourceToUse = clickedItem ? clickedItem.source : this.source;
|
||||
this.path = path;
|
||||
this.source = sourceToUse;
|
||||
filesApi.fetchFiles(sourceToUse, path).then(this.fillOptions);
|
||||
if (getters.isShare()) {
|
||||
publicApi.fetchPub(path, shareInfo.hash).then(this.fillOptions);
|
||||
} else {
|
||||
this.source = sourceToUse;
|
||||
filesApi.fetchFiles(sourceToUse, path).then(this.fillOptions);
|
||||
}
|
||||
|
||||
},
|
||||
touchstart(event) {
|
||||
let url = event.currentTarget.dataset.path;
|
||||
|
||||
@@ -3,17 +3,87 @@
|
||||
<h2>{{ $t("help.help") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("help.description") }} <a class="link" href="https://github.com/gtsteffaniak/filebrowser/wiki" target="_blank">{{ $t("help.wiki") }}</a></p>
|
||||
<ul>
|
||||
<li><strong>F1</strong> - {{ $t("help.f1") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<li><strong>F2</strong> - {{ $t("help.f2") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<li><strong>DEL</strong> - {{ $t("help.del") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<li><strong>ESC</strong> - {{ $t("help.esc") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<li><strong>CTRL + Click</strong> - {{ $t("help.ctrl.click") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<li><strong>Click</strong> - {{ $t("help.click") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<li><strong>CTRL + D</strong> - {{ $t("help.ctrl.d") }}</li> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
</ul>
|
||||
<div class="card-content help-content">
|
||||
<p class="help-description">
|
||||
{{ $t("help.description") }}
|
||||
<a class="link" href="https://filebrowserquantum.com/" target="_blank">
|
||||
{{ $t("help.wiki") }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div class="shortcuts-grid">
|
||||
<!-- Function Keys Section -->
|
||||
<div class="shortcut-section">
|
||||
<h3 class="section-title">Function Keys</h3> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<div class="shortcut-item">
|
||||
<kbd>F1</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.f1") }}</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>F2</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.f2") }}</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>DEL</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.del") }}</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>ESC</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.esc") }}</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>/</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.slash") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mouse Actions Section -->
|
||||
<div class="shortcut-section">
|
||||
<h3 class="section-title">Mouse Actions</h3> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<div class="shortcut-item">
|
||||
<kbd>Click</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.click") }}</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>CTRL + Click</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.ctrl.click") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Operations Section -->
|
||||
<div class="shortcut-section">
|
||||
<h3 class="section-title">File Operations</h3> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<div class="shortcut-item">
|
||||
<kbd>CTRL + C</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.ctrl.c") }}</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>CTRL + X</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.ctrl.x") }}</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>CTRL + V</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.ctrl.v") }}</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>CTRL + D</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.ctrl.d") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Section -->
|
||||
<div class="shortcut-section">
|
||||
<h3 class="section-title">Navigation</h3> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<div class="shortcut-item">
|
||||
<kbd>CTRL + B</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.ctrl.b") }}</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>CTRL + ,</kbd> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>{{ $t("help.ctrl.comma") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
@@ -36,3 +106,84 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.help-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.help-description {
|
||||
margin-bottom: 1.5em;
|
||||
color: var(--textSecondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.shortcuts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5em;
|
||||
}
|
||||
|
||||
.shortcut-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
color: var(--textPrimary);
|
||||
margin: 0 0 0.75em 0;
|
||||
padding-bottom: 0.5em;
|
||||
border-bottom: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75em;
|
||||
padding: 0.5em;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.shortcut-item:hover {
|
||||
background-color: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.shortcut-item kbd {
|
||||
display: inline-block;
|
||||
min-width: 80px;
|
||||
padding: 0.35em 0.65em;
|
||||
font-family: monospace;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: var(--textPrimary);
|
||||
background-color: var(--surfaceSecondary);
|
||||
border: 1px solid var(--divider);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shortcut-item span {
|
||||
flex: 1;
|
||||
font-size: 0.9em;
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.shortcuts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.shortcut-item kbd {
|
||||
min-width: 70px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
<i class="material-icons spin">sync</i>
|
||||
<p class="loading-text">{{ $t("prompts.operationInProgress") }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-else>
|
||||
<file-list ref="fileList" @update:selected="updateDestination">
|
||||
</file-list>
|
||||
</div>
|
||||
@@ -35,14 +35,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mutations, state } from "@/store";
|
||||
import { mutations, state, getters } from "@/store";
|
||||
import FileList from "./FileList.vue";
|
||||
import { filesApi } from "@/api";
|
||||
import { filesApi, publicApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { url } from "@/utils";
|
||||
import { notify } from "@/notify";
|
||||
import { removeLastDir, goToItem } from "@/utils/url";
|
||||
import { goToItem } from "@/utils/url";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "move-copy",
|
||||
@@ -73,19 +74,24 @@ export default {
|
||||
if (!this.destPath) {
|
||||
return false; // If dest is not set, we can't check containment
|
||||
}
|
||||
|
||||
// Add null checks to prevent undefined errors
|
||||
if (!this.items || this.items.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const itemPath = this.items[0]?.from;
|
||||
if (!itemPath) {
|
||||
return false; // If itemPath is undefined, we can't check containment
|
||||
}
|
||||
|
||||
const parentDir = (this.operation === "copy" ? url.removeLastDir(itemPath) : removeLastDir(itemPath)) + "/";
|
||||
return this.destPath == parentDir || this.destPath.startsWith(itemPath);
|
||||
// For move, prevent moving to the same directory, but for copy allow it.
|
||||
if (this.operation === "move") {
|
||||
const parentDir = url.removeLastDir(itemPath) + "/";
|
||||
// Only disable if moving to the exact same directory
|
||||
if (this.destPath === parentDir) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Prevent move/copy into itself or subdirectories (into itself too)
|
||||
return this.destPath.startsWith(itemPath + "/") || this.destPath === itemPath;
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
@@ -152,14 +158,20 @@ export default {
|
||||
item.toSource = this.destSource;
|
||||
}
|
||||
buttons.loading(this.operation);
|
||||
await filesApi.moveCopy(this.items, this.operation, overwrite, rename);
|
||||
if (getters.isShare()) {
|
||||
await publicApi.moveCopy(this.items, this.operation, overwrite, rename);
|
||||
} else {
|
||||
await filesApi.moveCopy(this.items, this.operation, overwrite, rename);
|
||||
}
|
||||
};
|
||||
// Fetch destination files
|
||||
let dstResp = await filesApi.fetchFiles(
|
||||
this.destSource,
|
||||
this.destPath
|
||||
);
|
||||
let conflict = upload.checkConflict(this.items, dstResp.items);
|
||||
let conflict = false;
|
||||
let dstResp = null;
|
||||
if (getters.isShare()) {
|
||||
dstResp = await publicApi.fetchPub(this.destPath, shareInfo.hash);
|
||||
} else {
|
||||
dstResp = await filesApi.fetchFiles(this.destSource, this.destPath);
|
||||
}
|
||||
conflict = upload.checkConflict(this.items, dstResp.items);
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
@@ -193,7 +205,7 @@ export default {
|
||||
}
|
||||
// Navigate to the destination folder after successful operation
|
||||
if (this.destSource && this.destPath) {
|
||||
goToItem(this.destSource, this.destPath);
|
||||
goToItem(this.destSource, this.destPath, {});
|
||||
}
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
async createDirectory(overwrite = false) {
|
||||
try {
|
||||
await filesApi.post(state.req.source, url.joinPath(state.req.path, this.name) + "/", "", overwrite);
|
||||
goToItem(state.req.source, url.joinPath(state.req.path, this.name));
|
||||
goToItem(state.req.source, url.joinPath(state.req.path, this.name), {});
|
||||
mutations.closeHovers();
|
||||
} catch (error) {
|
||||
if (error.message === "conflict") {
|
||||
@@ -90,7 +90,7 @@ export default {
|
||||
try {
|
||||
const newName = counter === 1 ? `${originalName} (1)` : `${originalName} (${counter})`;
|
||||
await filesApi.post(state.req.source, joinPath(state.req.path, newName) + "/", "", false);
|
||||
goToItem(state.req.source, joinPath(state.req.path, newName));
|
||||
goToItem(state.req.source, joinPath(state.req.path, newName), {});
|
||||
mutations.closeHovers();
|
||||
success = true;
|
||||
} catch (renameError) {
|
||||
|
||||
@@ -59,7 +59,7 @@ export default {
|
||||
async createFile(overwrite = false) {
|
||||
try {
|
||||
await filesApi.post(state.req.source, url.joinPath(state.req.path, this.name), "", overwrite);
|
||||
url.goToItem(state.req.source, url.joinPath(state.req.path, this.name));
|
||||
url.goToItem(state.req.source, url.joinPath(state.req.path, this.name), {});
|
||||
mutations.closeHovers();
|
||||
} catch (error) {
|
||||
if (error.message === "conflict") {
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
try {
|
||||
const newName = counter === 1 ? `${originalName} (1)` : `${originalName} (${counter})`;
|
||||
await filesApi.post(state.req.source, url.joinPath(state.req.path, newName), "", false);
|
||||
url.goToItem(state.req.source, url.joinPath(state.req.path, newName));
|
||||
url.goToItem(state.req.source, url.joinPath(state.req.path, newName), {});
|
||||
mutations.closeHovers();
|
||||
success = true;
|
||||
} catch (renameError) {
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<div class="card-title">
|
||||
<h2>
|
||||
{{ $t('player.QueuePlayback') }}
|
||||
<span class="queue-count-badge">{{ queueCount }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<!-- Playback mode display -->
|
||||
<div class="playback-mode">
|
||||
<div class="mode-info">
|
||||
<i class="material-icons">{{ currentModeIcon }}</i>
|
||||
<span>{{ currentModeLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue list -->
|
||||
<div v-if="formattedQueue.length > 0">
|
||||
<div class="file-list" ref="QueueList">
|
||||
<div
|
||||
v-for="(item, index) in formattedQueue"
|
||||
:key="`${item.path}-${index}`"
|
||||
class="listing-item file"
|
||||
:class="{ 'current': index === currentQueueIndex }"
|
||||
@click="navigateToItem(index)"
|
||||
>
|
||||
<div class="item-icon">
|
||||
<i class="material-icons">{{ getFileIcon(item) }}</i>
|
||||
</div>
|
||||
|
||||
<div class="item-name">
|
||||
<span class="name">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="item-indicator">
|
||||
<span v-if="index === currentQueueIndex" class="current-track">
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<i class="material-icons">{{ isPlaying ? 'pause' : 'play_arrow' }}</i>
|
||||
</span>
|
||||
<span v-else class="track-number">{{ index + 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Empty state -->
|
||||
<div v-else class="empty">
|
||||
<i class="material-icons">queue_music</i>
|
||||
<p>{{ $t('player.emptyQueue') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="button button--flat" @click.stop="cyclePlaybackModes" :title="$t('player.changePlaybackMode')">
|
||||
<i class="material-icons">swap_vert</i> {{ $t('player.changePlaybackMode') }}
|
||||
</button>
|
||||
|
||||
<button @click="closeModal" class="button button--flat" :aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')"> {{ $t('buttons.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, mutations } from "@/store";
|
||||
import { url } from "@/utils";
|
||||
export default {
|
||||
name: "PlaybackQueue",
|
||||
computed: {
|
||||
playbackQueue() {
|
||||
return state.playbackQueue?.queue || [];
|
||||
},
|
||||
currentQueueIndex() {
|
||||
return state.playbackQueue?.currentIndex ?? -1;
|
||||
},
|
||||
playbackMode() {
|
||||
return state.playbackQueue?.mode || 'single';
|
||||
},
|
||||
queueCount() {
|
||||
return this.playbackQueue.length;
|
||||
},
|
||||
currentModeLabel() {
|
||||
const modeLabels = {
|
||||
'single': this.$t('player.LoopDisabled'),
|
||||
'sequential': this.$t('player.PlayAllOncePlayback'),
|
||||
'shuffle': this.$t('player.ShuffleAllPlayback'),
|
||||
'loop-single': this.$t('player.LoopEnabled'),
|
||||
'loop-all': this.$t('player.PlayAllLoopedPlayback')
|
||||
};
|
||||
return modeLabels[this.playbackMode] || this.$t('player.LoopDisabled');
|
||||
},
|
||||
currentModeIcon() {
|
||||
const modeIcons = {
|
||||
'single': 'music_note',
|
||||
'sequential': 'playlist_play',
|
||||
'shuffle': 'shuffle',
|
||||
'loop-single': 'repeat_one',
|
||||
'loop-all': 'repeat'
|
||||
};
|
||||
return modeIcons[this.playbackMode] || 'music_note';
|
||||
},
|
||||
formattedQueue() {
|
||||
return this.playbackQueue.map((item) => ({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
type: item.type
|
||||
}));
|
||||
},
|
||||
isPlaying() {
|
||||
return state.playbackQueue?.isPlaying || false;
|
||||
},
|
||||
isPromptVisible() {
|
||||
// Check if this PlaybackQueue prompt is the current active prompt
|
||||
return state.prompts.some(prompt => prompt.name === 'PlaybackQueue');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentQueueIndex(newIndex, oldIndex) {
|
||||
// Auto-scroll when current item changes
|
||||
if (this.isPromptVisible && newIndex !== oldIndex) {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToCurrentItem();
|
||||
});
|
||||
}
|
||||
},
|
||||
isPromptVisible: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
// Prompt just became visible, scroll to current item
|
||||
console.log('PlaybackQueue prompt became visible, scrolling to current item');
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.scrollToCurrentItem();
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
playbackMode(newMode, oldMode) {
|
||||
// Auto-scroll when mode changes
|
||||
if (newMode !== oldMode) {
|
||||
this.$nextTick(() => {
|
||||
this.scrollToCurrentItem();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Auto-scroll to current item when prompt opens
|
||||
this.$nextTick(() => {
|
||||
this.scrollToCurrentItem();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
cyclePlaybackModes() {
|
||||
// Cycle through modes using store mutations
|
||||
const modes = ['loop-all', 'shuffle', 'sequential', 'loop-single'];
|
||||
const currentIndex = modes.indexOf(this.playbackMode);
|
||||
const nextMode = modes[(currentIndex + 1) % modes.length];
|
||||
|
||||
// Update store with new mode - this will trigger plyrViewer to rebuild queue
|
||||
mutations.setPlaybackQueue({
|
||||
queue: this.playbackQueue,
|
||||
currentIndex: this.currentQueueIndex,
|
||||
mode: nextMode
|
||||
});
|
||||
|
||||
// Auto-scroll after mode change
|
||||
this.$nextTick(() => {
|
||||
this.scrollToCurrentItem();
|
||||
});
|
||||
},
|
||||
|
||||
navigateToItem(index) {
|
||||
console.log('Navigate to item:', index);
|
||||
|
||||
if (index === this.currentQueueIndex) {
|
||||
// Toggle play/pause for current item
|
||||
this.togglePlayPause();
|
||||
} else {
|
||||
// Navigate to different item
|
||||
this.navigateToIndex(index);
|
||||
}
|
||||
},
|
||||
|
||||
togglePlayPause() {
|
||||
mutations.togglePlayPause();
|
||||
},
|
||||
|
||||
navigateToIndex(index) {
|
||||
if (index >= 0 && index < this.playbackQueue.length) {
|
||||
const item = this.playbackQueue[index];
|
||||
|
||||
// Update store with new current index
|
||||
mutations.setPlaybackQueue({
|
||||
queue: this.playbackQueue,
|
||||
currentIndex: index,
|
||||
mode: this.playbackMode
|
||||
});
|
||||
|
||||
// Close the prompt
|
||||
this.closeModal();
|
||||
|
||||
// Trigger actual navigation
|
||||
this.triggerNavigation(item);
|
||||
}
|
||||
},
|
||||
|
||||
triggerNavigation(item) {
|
||||
// Build the URL for the item
|
||||
const itemUrl = url.buildItemUrl(item.source || state.req.source, item.path);
|
||||
|
||||
// Update the current request in the store
|
||||
mutations.replaceRequest(item);
|
||||
|
||||
// Use router to navigate to the new item
|
||||
this.$router.replace({ path: itemUrl }).catch(err => {
|
||||
if (err.name !== 'NavigationDuplicated') {
|
||||
console.error('Router navigation error:', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
scrollToCurrentItem() {
|
||||
this.$nextTick(() => {
|
||||
const list = this.$refs.QueueList;
|
||||
const currentItem = list.querySelector('.listing-item.current');
|
||||
if (currentItem) {
|
||||
// Calculate the scroll position to center the current item
|
||||
const itemTop = currentItem.offsetTop;
|
||||
const itemHeight = currentItem.offsetHeight;
|
||||
const listHeight = list.clientHeight;
|
||||
const scrollTo = itemTop - (listHeight / 2) + (itemHeight / 2);
|
||||
list.scrollTo({
|
||||
top: Math.max(0, scrollTo),
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}});
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
mutations.closeHovers();
|
||||
},
|
||||
|
||||
getFileIcon(item) {
|
||||
if (item.type?.startsWith('audio/')) return 'audiotrack';
|
||||
if (item.type?.startsWith('video/')) return 'movie';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.queue-count-badge {
|
||||
background: var(--primaryColor);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
overflow: hidden !important;
|
||||
margin-top: 0;
|
||||
flex-direction: column;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.card-action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-action .button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mode-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mode-info i.material-icons {
|
||||
color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.playback-mode {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.listing-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
gap: 0.5rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.listing-item:hover {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.listing-item.current {
|
||||
background: var(--primaryColor);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.listing-item.current .item-icon i,
|
||||
.listing-item.current .current-indicator {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.item-icon i.material-icons {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
.item-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.track-number {
|
||||
color: var(--textSecondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
.empty i.material-icons {
|
||||
font-size: 3rem;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -31,7 +31,10 @@ import buttons from "@/utils/buttons";
|
||||
import Totp from "./Totp.vue";
|
||||
import Access from "./Access.vue";
|
||||
import Password from "./Password.vue";
|
||||
import PlaybackQueue from "./PlaybackQueue.vue";
|
||||
import FileList from "./FileList.vue";
|
||||
import SaveBeforeExit from "./SaveBeforeExit.vue";
|
||||
import CopyPasteConfirm from "./CopyPasteConfirm.vue";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
|
||||
export default {
|
||||
@@ -59,7 +62,10 @@ export default {
|
||||
ActionApi,
|
||||
Access,
|
||||
Password,
|
||||
PlaybackQueue,
|
||||
"file-list": FileList,
|
||||
SaveBeforeExit,
|
||||
CopyPasteConfirm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -97,6 +103,9 @@ export default {
|
||||
case "replace":
|
||||
prompt.showConfirm(event);
|
||||
break;
|
||||
case "CopyPasteConfirm":
|
||||
prompt.confirm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,10 +29,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { filesApi } from "@/api";
|
||||
import { mutations, state } from "@/store";
|
||||
import { filesApi, publicApi } from "@/api";
|
||||
import { mutations, state, getters } from "@/store";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "rename",
|
||||
props: {
|
||||
@@ -118,7 +117,11 @@ export default {
|
||||
toSource: this.item.source,
|
||||
}];
|
||||
|
||||
await filesApi.moveCopy(items, "move");
|
||||
if (getters.isShare()) {
|
||||
await publicApi.moveCopy(items, "move");
|
||||
} else {
|
||||
await filesApi.moveCopy(items, "move");
|
||||
}
|
||||
mutations.closeHovers();
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.saveBeforeExit") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.saveBeforeExitMessage") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="keepEditing"
|
||||
:aria-label="$t('buttons.keepEditing')"
|
||||
:title="$t('buttons.keepEditing')">
|
||||
{{ $t("buttons.keepEditing") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--blue"
|
||||
@click="discardAndExit"
|
||||
:aria-label="$t('buttons.discardAndExit')"
|
||||
:title="$t('buttons.discardAndExit')">
|
||||
{{ $t("buttons.discardAndExit") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="saveAndExit"
|
||||
:aria-label="$t('buttons.saveAndExit')"
|
||||
:title="$t('buttons.saveAndExit')">
|
||||
{{ $t("buttons.saveAndExit") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getters, mutations } from "@/store";
|
||||
|
||||
export default {
|
||||
name: "saveBeforeExit",
|
||||
computed: {
|
||||
currentPrompt() {
|
||||
return getters.currentPrompt();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// Listen for ESC key to treat it as "Keep Editing"
|
||||
this.escKeyHandler = (event) => {
|
||||
if (event.keyCode === 27 && getters.currentPromptName() === "SaveBeforeExit") {
|
||||
event.stopImmediatePropagation();
|
||||
this.keepEditing();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", this.escKeyHandler, true);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("keydown", this.escKeyHandler, true);
|
||||
},
|
||||
methods: {
|
||||
keepEditing() {
|
||||
if (this.currentPrompt && this.currentPrompt.cancel) {
|
||||
this.currentPrompt.cancel();
|
||||
}
|
||||
mutations.closeTopHover();
|
||||
},
|
||||
async saveAndExit() {
|
||||
if (this.currentPrompt && this.currentPrompt.confirm) {
|
||||
try {
|
||||
await this.currentPrompt.confirm();
|
||||
// Only close prompt if save succeeded
|
||||
mutations.closeTopHover();
|
||||
} catch (error) {
|
||||
// If save fails, keep the prompt open so user can try again or choose another option
|
||||
console.error("Save failed:", error);
|
||||
// The error notification is already shown by the save handler
|
||||
}
|
||||
}
|
||||
},
|
||||
discardAndExit() {
|
||||
if (this.currentPrompt && this.currentPrompt.discard) {
|
||||
this.currentPrompt.discard();
|
||||
}
|
||||
mutations.closeTopHover();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -98,10 +98,11 @@
|
||||
</select>
|
||||
|
||||
<div class="settings-items" style="margin-top: 0.5em;">
|
||||
<!--
|
||||
<ToggleSwitch class="item" v-model="allowEdit" :name="'Allow modifications'" />
|
||||
<ToggleSwitch class="item" v-model="allowUpload" :name="'Allow uploading'" />
|
||||
-->
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="disableDownload" :name="$t('share.disableDownload')" :description="$t('share.disableDownloadDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowModify" :name="$t('share.allowModify')" :description="$t('share.allowModifyDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowDelete" :name="$t('share.allowDelete')" :description="$t('share.allowDeleteDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowCreate" :name="$t('share.allowCreate')" :description="$t('share.allowCreateDescription')" />
|
||||
<ToggleSwitch v-if="createAllowed" class="item" v-model="allowReplacements" :name="$t('share.allowReplacements')" :description="$t('share.allowReplacementsDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="disableFileViewer" :name="$t('share.disableFileViewer')" />
|
||||
<ToggleSwitch
|
||||
v-if="shareType === 'normal'"
|
||||
@@ -338,12 +339,15 @@ export default {
|
||||
clip: null,
|
||||
password: "",
|
||||
listing: true,
|
||||
allowEdit: false,
|
||||
allowModify: false,
|
||||
disableDownload: false,
|
||||
allowDelete: false,
|
||||
allowCreate: false,
|
||||
allowReplacements: false,
|
||||
downloadsLimit: "",
|
||||
perUserDownloadLimit: false,
|
||||
shareTheme: "default",
|
||||
disableAnonymous: false,
|
||||
allowUpload: false,
|
||||
maxBandwidth: "",
|
||||
disableFileViewer: false,
|
||||
disableThumbnails: false,
|
||||
@@ -371,6 +375,9 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
createAllowed() {
|
||||
return this.allowCreate;
|
||||
},
|
||||
displayPath() {
|
||||
// When editing, use the link's path; otherwise use the item's path
|
||||
return this.isEditMode ? this.link.path : this.item.path;
|
||||
@@ -435,6 +442,11 @@ export default {
|
||||
: "0";
|
||||
this.unit = "hours";
|
||||
this.password = "";
|
||||
this.disableDownload = this.link.disableDownload || false;
|
||||
this.allowModify = this.link.allowModify || false;
|
||||
this.allowDelete = this.link.allowDelete || false;
|
||||
this.allowCreate = this.link.allowCreate || false;
|
||||
this.allowReplacements = this.link.allowReplacements || false;
|
||||
this.downloadsLimit = this.link.downloadsLimit ? String(this.link.downloadsLimit) : "";
|
||||
this.perUserDownloadLimit = this.link.perUserDownloadLimit || false;
|
||||
this.maxBandwidth = this.link.maxBandwidth ? String(this.link.maxBandwidth) : "";
|
||||
@@ -524,7 +536,11 @@ export default {
|
||||
expires: isPermanent ? "" : this.time.toString(),
|
||||
unit: this.unit,
|
||||
disableAnonymous: this.disableAnonymous,
|
||||
allowUpload: this.allowUpload,
|
||||
disableDownload: this.disableDownload,
|
||||
allowModify: this.allowModify,
|
||||
allowDelete: this.allowDelete,
|
||||
allowCreate: this.allowCreate,
|
||||
allowReplacements: this.allowReplacements,
|
||||
maxBandwidth: this.maxBandwidth ? parseInt(this.maxBandwidth) : 0,
|
||||
downloadsLimit: this.downloadsLimit ? parseInt(this.downloadsLimit) : 0,
|
||||
perUserDownloadLimit: this.perUserDownloadLimit,
|
||||
@@ -595,6 +611,11 @@ export default {
|
||||
: "0";
|
||||
this.unit = "hours";
|
||||
this.password = "";
|
||||
this.disableDownload = link.disableDownload || false;
|
||||
this.allowModify = link.allowModify || false;
|
||||
this.allowDelete = link.allowDelete || false;
|
||||
this.allowCreate = link.allowCreate || false;
|
||||
this.allowReplacements = link.allowReplacements || false;
|
||||
this.downloadsLimit = link.downloadsLimit ? String(link.downloadsLimit) : "";
|
||||
this.perUserDownloadLimit = link.perUserDownloadLimit || false;
|
||||
this.maxBandwidth = link.maxBandwidth ? String(link.maxBandwidth) : "";
|
||||
|
||||
@@ -172,17 +172,14 @@ export default {
|
||||
},
|
||||
uploadSettingsDescription() {
|
||||
const maxConcurrentUpload = state.user.fileLoading?.maxConcurrentUpload || 3;
|
||||
const uploadChunkSizeMb = state.user.fileLoading?.uploadChunkSizeMb || 0;
|
||||
if (uploadChunkSizeMb > 0) {
|
||||
return this.$t("prompts.uploadSettingsChunked", {
|
||||
maxConcurrentUpload,
|
||||
uploadChunkSizeMb
|
||||
});
|
||||
} else {
|
||||
return this.$t("prompts.uploadSettingsNoChunk", {
|
||||
maxConcurrentUpload
|
||||
});
|
||||
let uploadChunkSizeMb = state.user.fileLoading?.uploadChunkSizeMb || 5;
|
||||
if (uploadChunkSizeMb === 0) {
|
||||
uploadChunkSizeMb = 5;
|
||||
}
|
||||
return this.$t("prompts.uploadSettingsChunked", {
|
||||
maxConcurrentUpload,
|
||||
uploadChunkSizeMb
|
||||
});
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
@@ -197,7 +194,10 @@ export default {
|
||||
|
||||
// Upload settings
|
||||
const maxConcurrentUpload = ref(state.user.fileLoading?.maxConcurrentUpload || 3);
|
||||
const uploadChunkSizeMb = ref(state.user.fileLoading?.uploadChunkSizeMb || 0);
|
||||
const uploadChunkSizeMb = ref(state.user.fileLoading?.uploadChunkSizeMb || 5);
|
||||
if (uploadChunkSizeMb.value === 0) {
|
||||
uploadChunkSizeMb.value = 5;
|
||||
}
|
||||
const clearAll = ref(state.user.fileLoading?.clearAll || false);
|
||||
|
||||
const showTooltip = (event, text) => {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
<div class="card-content">
|
||||
<errors v-if="error" :errorCode="error.status" />
|
||||
|
||||
<h2
|
||||
class="message"
|
||||
v-if="user.loginMethod != 'password' && !stateUser.permissions.admin"
|
||||
@@ -17,7 +16,6 @@
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t("files.lonely") }}</span>
|
||||
</h2>
|
||||
|
||||
<div v-if="user.loginMethod == 'password' && globalVars.passwordAvailable && !isNew">
|
||||
<label for="password">{{ $t("settings.password") }}</label>
|
||||
<div class="form-flex-group">
|
||||
@@ -63,7 +61,6 @@
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div v-if="stateUser.permissions.admin">
|
||||
<p v-if="isNew">
|
||||
<label for="username">{{ $t("settings.username") }}</label>
|
||||
@@ -333,7 +330,7 @@ export default {
|
||||
if (this.globalVars.passwordAvailable) validMethods.push("password");
|
||||
if (this.globalVars.oidcAvailable) validMethods.push("oidc");
|
||||
if (this.globalVars.proxyAvailable) validMethods.push("proxy");
|
||||
|
||||
|
||||
if (!this.user.loginMethod || !validMethods.includes(this.user.loginMethod)) {
|
||||
this.user.loginMethod = this.firstAvailableLoginMethod;
|
||||
}
|
||||
@@ -349,7 +346,7 @@ export default {
|
||||
if (this.globalVars.passwordAvailable) validMethods.push("password");
|
||||
if (this.globalVars.oidcAvailable) validMethods.push("oidc");
|
||||
if (this.globalVars.proxyAvailable) validMethods.push("proxy");
|
||||
|
||||
|
||||
if (!this.user.loginMethod || !validMethods.includes(this.user.loginMethod)) {
|
||||
this.user.loginMethod = this.firstAvailableLoginMethod;
|
||||
}
|
||||
@@ -499,9 +496,9 @@ export default {
|
||||
if (this.globalVars.passwordAvailable) validMethods.push("password");
|
||||
if (this.globalVars.oidcAvailable) validMethods.push("oidc");
|
||||
if (this.globalVars.proxyAvailable) validMethods.push("proxy");
|
||||
|
||||
|
||||
const isValidMethod = validMethods.includes(this.user.loginMethod);
|
||||
|
||||
|
||||
if (!this.user.loginMethod || this.user.loginMethod === null || !isValidMethod) {
|
||||
this.user.loginMethod = this.firstAvailableLoginMethod;
|
||||
}
|
||||
|
||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user