mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-04 16:50:08 -05:00
Compare commits
132
Commits
main
...
v1.2.1-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edaa8c536d | ||
|
|
52d57017f2 | ||
|
|
1dd219ad18 | ||
|
|
e584c30225 | ||
|
|
801c029b03 | ||
|
|
bf46e977c4 | ||
|
|
c02a994880 | ||
|
|
2cf352876c | ||
|
|
fb2c97d05a | ||
|
|
0bd58c14ee | ||
|
|
09376139f1 | ||
|
|
59495b7f2f | ||
|
|
d258333f26 | ||
|
|
fe57dd5edc | ||
|
|
b96636279e | ||
|
|
3025442ff8 | ||
|
|
715a044af1 | ||
|
|
a28d515b90 | ||
|
+4 |
990d53fa95 | ||
|
|
f0068d0765 | ||
|
|
8fd13a40bc | ||
|
|
941841fca1 | ||
|
|
0235546a8f | ||
|
|
66482cd1d5 | ||
|
|
d26189968f | ||
|
|
eb518bac56 | ||
|
|
d82e7f2986 | ||
|
|
7cd3f8374e | ||
|
|
4e0f0453f3 | ||
|
|
98246b2981 | ||
|
|
d2c07ffd3c | ||
|
|
43cbadbeb8 | ||
|
|
25dc426522 | ||
|
|
27dbd509b2 | ||
|
|
3716c4e4c6 | ||
|
|
7aabdadf3e | ||
|
|
264de1ce88 | ||
|
|
4aae24cf11 | ||
|
|
0968c44d22 | ||
|
|
a15fe4eaa5 | ||
|
|
a433a8a8d4 | ||
|
|
b5219ae6b6 | ||
|
|
d9f91c7c65 | ||
|
|
4a85746850 | ||
|
|
3ae8aede25 | ||
|
|
3990890721 | ||
|
|
726116d831 | ||
|
|
8274f2c715 | ||
|
|
32969d0777 | ||
|
|
8f25186136 | ||
|
|
e18a73c97a | ||
|
|
e5cb88ba5d | ||
|
|
50b9ee8b31 | ||
|
|
a0e266fadf | ||
|
|
377ae33e1b | ||
|
|
e14f2d7a4b | ||
|
|
fac091f1b3 | ||
|
|
5330d6b093 | ||
|
|
692f99251d | ||
|
|
31e70519c1 | ||
|
|
0b88a90048 | ||
|
|
79d7f07943 | ||
|
|
b562edaaeb | ||
|
|
33ede042da | ||
|
|
036a0ed6c8 | ||
|
|
1842ff561b | ||
|
|
aed9f7c135 | ||
|
|
c6600a0665 | ||
|
|
3e3d3decc6 | ||
|
|
c58b298eb5 | ||
|
|
98ba8095db | ||
|
|
68fb573059 | ||
|
|
5747c40dca | ||
|
|
598bd366db | ||
|
|
e3be3300dc | ||
|
|
416aa75446 | ||
|
|
cfb3b9e869 | ||
|
|
166f1bf232 | ||
|
|
451f9ac69f | ||
|
|
b2fa1780ed | ||
|
|
9255bc174b | ||
|
|
a3a074d7b6 | ||
|
|
aefe16b508 | ||
|
|
728ee72e2a | ||
|
|
e94075de6c | ||
|
|
c56bafb4e9 | ||
|
|
a6e045cc85 | ||
|
|
690685459e | ||
|
|
6f6244b2fb | ||
|
|
05fcd31909 | ||
|
|
a4107a734a | ||
|
|
991be79521 | ||
|
|
8755e82780 | ||
|
|
d1b3a045af | ||
|
|
b5b365c6a7 | ||
|
|
c33a876033 | ||
|
|
42778b92bb | ||
|
|
ff3b66b79d | ||
|
|
daedeef78d | ||
|
|
edba1a80ef | ||
|
|
77fb6bdabc | ||
|
|
97c37caa1c | ||
|
|
02b28284ca | ||
|
|
6a489c6f7c | ||
|
|
ba443398ca | ||
|
|
17ff68c00d | ||
|
|
cb71dd8940 | ||
|
|
764155d612 | ||
|
|
9013152d9d | ||
|
|
27bc000cda | ||
|
|
8f4e420e73 | ||
|
|
e4d8a5cf20 | ||
|
|
fa8ceb50e6 | ||
|
|
c8073db4e6 | ||
|
|
23fac83e7e | ||
|
|
f36ca31c7f | ||
|
|
c0466e96fc | ||
|
|
dc753fe326 | ||
|
|
c39a1937b4 | ||
|
|
5d38b1e66a | ||
|
|
e5e93b4b70 | ||
|
|
eaedbbe05e | ||
|
|
c6d5f32bd0 | ||
|
|
ee022bc4a9 | ||
|
|
daabefa6c3 | ||
|
|
86aebde566 | ||
|
|
a3b0fc0038 | ||
|
|
695364c2df | ||
|
|
58a01ff986 | ||
|
|
3298111560 | ||
|
|
3ca9f54de0 | ||
|
|
97d16d1663 |
No files matched your search
@@ -2,6 +2,25 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
|
||||
## v1.2.1-beta
|
||||
|
||||
**New Features**:
|
||||
- Global disable onlyoffice editor via "*" file option to disable all files for a specific user. (#1533)
|
||||
|
||||
**Notes**:
|
||||
- upgraded imaging package and improved thumbnail generation performance. (#1797) (#1850
|
||||
)
|
||||
- updated download api to use repeated `file` query param for path instead of `files` with comma. See swagger for details (#1881)
|
||||
|
||||
**BugFixes**:
|
||||
- OnlyOffice stopped working after 1.2.0 upgrade due to download api changes
|
||||
- Search result link to wrong path when search context other than / (#1863)
|
||||
- File actions doesn't work if using user scopes (#1879)
|
||||
- Drop items into folders doesn't work (#1860)
|
||||
- duplicate finder context menu (#1862)
|
||||
- File list in previews doesn't show the parent items (#1861)
|
||||
|
||||
## v1.2.0-beta
|
||||
|
||||
This is a major version update with many code changes which could cause unexpected behavior. Upgrades should proceed with caution and report any undesriable behavior by opening an issue on github. The index is now fully in sqlite database - see [Indexing Overview](https://filebrowserquantum.com/en/docs/features/indexing/) for more information.
|
||||
|
||||
@@ -104,12 +104,12 @@ Mac | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
Self hostable | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Has Stable Release? | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
S3 support | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
|
||||
webdav support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
webdav support | :warning:* | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
FTP support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Dedicated docs site? | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Multiple sources at once | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Docker image size | 180 MB (with ffmpeg) | 31 MB | 240 MB (main image) | 250 MB | ❌ | > 2 GB |
|
||||
Min. Memory Requirements | 256 MB | 128 MB | 128 MB (main image) | 512 MB | ❌ | 512 MB |
|
||||
Min. Memory Requirements | 512 MB | 128 MB | 128 MB (main image) | 512 MB | ❌ | 512 MB |
|
||||
has standalone binary | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
price | free | free | free | free tier | free tier | $99+ |
|
||||
rich media preview | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
@@ -146,3 +146,5 @@ Can browse archive files | :construction: | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Can convert documents | :construction: | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
Can convert videos | :construction: | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
Can convert photos | :construction: | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
*Accessing files from FileBrowser via WebDav is a newly introduced and experimental feature.
|
||||
@@ -208,7 +208,7 @@ func TestOverrideDirectoryToFile(t *testing.T) {
|
||||
|
||||
// Initialize the database first (use test helper to avoid permission issues)
|
||||
if indexing.GetIndexDB() == nil {
|
||||
db, err := dbsql.NewIndexDB("test_file", "OFF", 1000, 32, false)
|
||||
db, _, err := dbsql.NewIndexDB("test_file", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
@@ -318,7 +318,7 @@ func TestOverrideFileToDirectory(t *testing.T) {
|
||||
|
||||
// Initialize the database first (use test helper to avoid permission issues)
|
||||
if indexing.GetIndexDB() == nil {
|
||||
db, err := dbsql.NewIndexDB("test_file", "OFF", 1000, 32, false)
|
||||
db, _, err := dbsql.NewIndexDB("test_file", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
|
||||
+13
-1
@@ -116,13 +116,23 @@ func StartFilebrowser() {
|
||||
}
|
||||
|
||||
// Initialize shared index database before starting HTTP service
|
||||
if err := indexing.InitializeIndexDB(); err != nil {
|
||||
wasRecreated, err := indexing.InitializeIndexDB()
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed to initialize index database: %v", err)
|
||||
}
|
||||
|
||||
// Set indexing storage for persistence
|
||||
if store != nil && store.Indexing != nil {
|
||||
indexing.SetIndexingStorage(store.Indexing)
|
||||
|
||||
// If the index database was recreated (fresh or corrupted), reset all complexities
|
||||
if wasRecreated {
|
||||
if err := store.Indexing.ResetAllComplexities(); err != nil {
|
||||
logger.Errorf("Failed to reset index complexities: %v", err)
|
||||
} else {
|
||||
logger.Infof("Successfully reset all index complexities to 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, source := range settings.Config.Server.SourceMap {
|
||||
@@ -180,6 +190,8 @@ func rootCMD(ctx context.Context, store *bolt.BoltStore, serverConfig *settings.
|
||||
if err != nil {
|
||||
logger.Fatalf("Error starting preview service: %v", err)
|
||||
}
|
||||
logger.Debugf("MuPDF Enabled : %v", settings.Env.MuPdfAvailable)
|
||||
logger.Debugf("Media Enabled : %v", settings.MediaEnabled())
|
||||
fbhttp.StartHttp(ctx, store, shutdownComplete)
|
||||
return nil
|
||||
}
|
||||
@@ -281,39 +281,37 @@ func setupFrontend(generate bool) {
|
||||
}
|
||||
|
||||
func setupMedia() {
|
||||
// If VideoPreview is not initialized, initialize with all types enabled
|
||||
if Config.Integrations.Media.Convert.VideoPreview == nil {
|
||||
Config.Integrations.Media.Convert.VideoPreview = make(map[VideoPreviewType]bool)
|
||||
for _, t := range AllVideoPreviewTypes {
|
||||
Config.Integrations.Media.Convert.VideoPreview[t] = true
|
||||
}
|
||||
return
|
||||
// Save user's explicit config before applying defaults
|
||||
userImageConfig := make(map[ImagePreviewType]*bool)
|
||||
for k, v := range Config.Integrations.Media.Convert.ImagePreview {
|
||||
userImageConfig[k] = v
|
||||
}
|
||||
|
||||
// If VideoPreview map is empty, it means user didn't configure any video preview settings
|
||||
// In this case, enable all by default
|
||||
if len(Config.Integrations.Media.Convert.VideoPreview) == 0 {
|
||||
for _, t := range AllVideoPreviewTypes {
|
||||
Config.Integrations.Media.Convert.VideoPreview[t] = true
|
||||
}
|
||||
return
|
||||
// Re-initialize with defaults (gets wiped out during YAML unmarshal)
|
||||
Config.Integrations.Media.Convert.ImagePreview = make(map[ImagePreviewType]*bool)
|
||||
Config.Integrations.Media.Convert.ImagePreview[HEICImagePreview] = boolPtr(false) // HEIC defaults to disabled
|
||||
Config.Integrations.Media.Convert.ImagePreview[JPEGImagePreview] = boolPtr(true) // JPEG fallback defaults to enabled
|
||||
|
||||
// Apply user overrides (only for keys explicitly set in YAML)
|
||||
for k, v := range userImageConfig {
|
||||
Config.Integrations.Media.Convert.ImagePreview[k] = v
|
||||
}
|
||||
|
||||
// User has explicitly configured some video preview settings
|
||||
// Start with all enabled, then apply user overrides
|
||||
userConfig := make(map[VideoPreviewType]bool)
|
||||
// VideoPreview: Merge user config with defaults
|
||||
// Save user's explicit config from YAML
|
||||
userVideoConfig := make(map[VideoPreviewType]*bool)
|
||||
for k, v := range Config.Integrations.Media.Convert.VideoPreview {
|
||||
userConfig[k] = v
|
||||
userVideoConfig[k] = v
|
||||
}
|
||||
|
||||
// Reset to defaults (all enabled)
|
||||
Config.Integrations.Media.Convert.VideoPreview = make(map[VideoPreviewType]bool)
|
||||
// Re-initialize with defaults (all enabled)
|
||||
Config.Integrations.Media.Convert.VideoPreview = make(map[VideoPreviewType]*bool)
|
||||
for _, t := range AllVideoPreviewTypes {
|
||||
Config.Integrations.Media.Convert.VideoPreview[t] = true
|
||||
Config.Integrations.Media.Convert.VideoPreview[t] = boolPtr(true)
|
||||
}
|
||||
|
||||
// Apply user overrides (only for explicitly set values)
|
||||
for k, v := range userConfig {
|
||||
// Apply user overrides (only for keys explicitly set in YAML)
|
||||
for k, v := range userVideoConfig {
|
||||
Config.Integrations.Media.Convert.VideoPreview[k] = v
|
||||
}
|
||||
}
|
||||
@@ -680,7 +678,6 @@ func setDefaults(generate bool) Settings {
|
||||
Frontend: Frontend{
|
||||
Name: "FileBrowser Quantum",
|
||||
},
|
||||
|
||||
UserDefaults: UserDefaults{
|
||||
DisableOnlyOfficeExt: ".md .txt .pdf .html .xml",
|
||||
StickySidebar: true,
|
||||
@@ -710,21 +707,20 @@ func setDefaults(generate bool) Settings {
|
||||
Folder: boolPtr(true),
|
||||
},
|
||||
FileLoading: users.FileLoading{
|
||||
MaxConcurrent: 10,
|
||||
UploadChunkSize: 10, // 10MB
|
||||
MaxConcurrent: 10,
|
||||
UploadChunkSize: 10, // 10MB
|
||||
DownloadChunkSize: 0, // 0MB, default to no chunking
|
||||
},
|
||||
},
|
||||
}
|
||||
// Initialize ImagePreview map with all supported types set to false by default
|
||||
s.Integrations.Media.Convert.ImagePreview = make(map[ImagePreviewType]bool)
|
||||
for _, t := range AllImagePreviewTypes {
|
||||
s.Integrations.Media.Convert.ImagePreview[t] = false
|
||||
}
|
||||
|
||||
// Initialize VideoPreview map with all supported types set to true by default
|
||||
s.Integrations.Media.Convert.VideoPreview = make(map[VideoPreviewType]bool)
|
||||
// Initialize ImagePreview map with defaults
|
||||
s.Integrations.Media.Convert.ImagePreview = make(map[ImagePreviewType]*bool)
|
||||
s.Integrations.Media.Convert.ImagePreview[HEICImagePreview] = boolPtr(false) // HEIC defaults to disabled
|
||||
s.Integrations.Media.Convert.ImagePreview[JPEGImagePreview] = boolPtr(true) // JPEG fallback defaults to enabled
|
||||
// Initialize VideoPreview map with defaults (all enabled)
|
||||
s.Integrations.Media.Convert.VideoPreview = make(map[VideoPreviewType]*bool)
|
||||
for _, t := range AllVideoPreviewTypes {
|
||||
s.Integrations.Media.Convert.VideoPreview[t] = true
|
||||
s.Integrations.Media.Convert.VideoPreview[t] = boolPtr(true)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
package settings
|
||||
|
||||
func CanConvertImage(ext string) bool {
|
||||
return MediaEnabled() && Config.Integrations.Media.Convert.ImagePreview[ImagePreviewType(ext)]
|
||||
if !MediaEnabled() {
|
||||
return false
|
||||
}
|
||||
val := Config.Integrations.Media.Convert.ImagePreview[ImagePreviewType(ext)]
|
||||
if val == nil {
|
||||
return false // Extension not in the configured list
|
||||
}
|
||||
return *val
|
||||
}
|
||||
|
||||
func CanConvertVideo(ext string) bool {
|
||||
return MediaEnabled() && Config.Integrations.Media.Convert.VideoPreview[VideoPreviewType(ext)]
|
||||
if !MediaEnabled() {
|
||||
return false
|
||||
}
|
||||
val := Config.Integrations.Media.Convert.VideoPreview[VideoPreviewType(ext)]
|
||||
if val == nil {
|
||||
return false // Extension not in the configured list
|
||||
}
|
||||
return *val
|
||||
}
|
||||
|
||||
func MediaEnabled() bool {
|
||||
|
||||
@@ -649,11 +649,6 @@ func buildNodeWithDefaults(v reflect.Value, comm CommentsMap, defaults reflect.V
|
||||
}
|
||||
mapNode := &yaml.Node{Kind: yaml.MappingNode}
|
||||
|
||||
// Special handling for ImagePreview maps - show all supported types
|
||||
if isImagePreviewMap(v) {
|
||||
return buildImagePreviewMap(v, comm, secrets, deprecated)
|
||||
}
|
||||
|
||||
// Get and sort map keys alphabetically for consistent output
|
||||
mapKeys := v.MapKeys()
|
||||
keyStrings := make([]string, len(mapKeys))
|
||||
@@ -707,10 +702,8 @@ func GenerateYaml() {
|
||||
{
|
||||
Path: ".",
|
||||
Config: SourceConfig{
|
||||
Conditionals: ConditionalFilter{
|
||||
ItemRules: []ConditionalRule{
|
||||
{}, // Add empty sample rule to show all fields in generated YAML
|
||||
},
|
||||
Rules: []ConditionalRule{
|
||||
{}, // Add empty sample rule to show all fields in generated YAML
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1136,53 +1129,3 @@ func AlignComments(input string) string {
|
||||
|
||||
return strings.Join(outputLines, "\n")
|
||||
}
|
||||
|
||||
// isImagePreviewMap checks if the given map is an ImagePreview map
|
||||
func isImagePreviewMap(v reflect.Value) bool {
|
||||
if v.Kind() != reflect.Map {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the map key type is ImagePreviewType
|
||||
keyType := v.Type().Key()
|
||||
return keyType.Name() == "ImagePreviewType"
|
||||
}
|
||||
|
||||
// buildImagePreviewMap builds a YAML node for ImagePreview maps, showing all supported types
|
||||
func buildImagePreviewMap(v reflect.Value, comm CommentsMap, secrets SecretFieldsMap, deprecated DeprecatedFieldsMap) (*yaml.Node, error) {
|
||||
mapNode := &yaml.Node{Kind: yaml.MappingNode}
|
||||
|
||||
// Get all supported image preview types from the global slice
|
||||
for _, imageType := range AllImagePreviewTypes {
|
||||
typeName := string(imageType)
|
||||
|
||||
// Create key node
|
||||
keyNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: typeName,
|
||||
}
|
||||
|
||||
// Get the value from the map, defaulting to false if not present
|
||||
var value bool
|
||||
if v.Len() > 0 {
|
||||
// Look for the key in the map
|
||||
for _, mapKey := range v.MapKeys() {
|
||||
if mapKey.String() == typeName {
|
||||
value = v.MapIndex(mapKey).Bool()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create value node
|
||||
valNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!bool",
|
||||
Value: fmt.Sprintf("%t", value),
|
||||
}
|
||||
|
||||
mapNode.Content = append(mapNode.Content, keyNode, valNode)
|
||||
}
|
||||
|
||||
return mapNode, nil
|
||||
}
|
||||
@@ -101,14 +101,15 @@ type Media struct {
|
||||
}
|
||||
|
||||
type FfmpegConvert struct {
|
||||
ImagePreview map[ImagePreviewType]bool `json:"imagePreview"` // supported image preview formats. defaults to false for all types unless explicitly enabled.
|
||||
VideoPreview map[VideoPreviewType]bool `json:"videoPreview"` // supported video preview formats. defaults to true for all types unless explicitly disabled.
|
||||
ImagePreview map[ImagePreviewType]*bool `json:"imagePreview"` // supported image preview formats. defaults vary by type (see individual type docs)
|
||||
VideoPreview map[VideoPreviewType]*bool `json:"videoPreview"` // supported video preview formats. defaults to true for all types unless explicitly disabled.
|
||||
}
|
||||
|
||||
type ImagePreviewType string
|
||||
|
||||
const (
|
||||
HEICImagePreview ImagePreviewType = "heic"
|
||||
JPEGImagePreview ImagePreviewType = "jpeg" // only used as fallback for JPEG formats that can't otherwise be decoded
|
||||
//RAWImagePreview ImagePreviewType = "raw"
|
||||
)
|
||||
|
||||
@@ -119,6 +120,7 @@ func (i ImagePreviewType) String() string {
|
||||
// AllImagePreviewTypes contains all supported image preview types.
|
||||
var AllImagePreviewTypes = []ImagePreviewType{
|
||||
HEICImagePreview,
|
||||
JPEGImagePreview,
|
||||
//RAWImagePreview,
|
||||
}
|
||||
|
||||
@@ -350,5 +352,4 @@ type UserDefaults struct {
|
||||
CustomTheme string `json:"customTheme"` // Name of theme to use chosen from custom themes config.
|
||||
ShowSelectMultiple bool `json:"showSelectMultiple"` // show select multiple files on desktop
|
||||
DebugOffice bool `json:"debugOffice"` // debug onlyoffice editor
|
||||
DefaultLandingPage string `json:"defaultLandingPage"` // deprecated: determined by sidebar link order since 1.1.0
|
||||
}
|
||||
@@ -26,7 +26,7 @@ integrations:
|
||||
testVideoPreviewConfig(t, config, func(t *testing.T, config *Settings) {
|
||||
// All video formats should be enabled by default
|
||||
for _, format := range AllVideoPreviewTypes {
|
||||
if !config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
if !*config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
t.Errorf("Expected %s to be enabled by default, but it was disabled", format)
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ integrations:
|
||||
testVideoPreviewConfig(t, config, func(t *testing.T, config *Settings) {
|
||||
// All video formats should be enabled by default
|
||||
for _, format := range AllVideoPreviewTypes {
|
||||
if !config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
if !*config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
t.Errorf("Expected %s to be enabled by default, but it was disabled", format)
|
||||
}
|
||||
}
|
||||
@@ -81,16 +81,16 @@ integrations:
|
||||
`
|
||||
testVideoPreviewConfig(t, config, func(t *testing.T, config *Settings) {
|
||||
// Check explicitly set formats
|
||||
if !config.Integrations.Media.Convert.VideoPreview[MP4VideoPreview] {
|
||||
if !*config.Integrations.Media.Convert.VideoPreview[MP4VideoPreview] {
|
||||
t.Error("Expected mp4 to be enabled")
|
||||
}
|
||||
if config.Integrations.Media.Convert.VideoPreview[WebMVideoPreview] {
|
||||
if *config.Integrations.Media.Convert.VideoPreview[WebMVideoPreview] {
|
||||
t.Error("Expected webm to be disabled")
|
||||
}
|
||||
if config.Integrations.Media.Convert.VideoPreview[MKVVideoPreview] {
|
||||
if *config.Integrations.Media.Convert.VideoPreview[MKVVideoPreview] {
|
||||
t.Error("Expected mkv to be disabled")
|
||||
}
|
||||
if config.Integrations.Media.Convert.VideoPreview[WMVVideoPreview] {
|
||||
if *config.Integrations.Media.Convert.VideoPreview[WMVVideoPreview] {
|
||||
t.Error("Expected wmv to be disabled")
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ integrations:
|
||||
M4VVideoPreview, ThreeGPVideoPreview, TSVideoPreview, VOBVideoPreview,
|
||||
}
|
||||
for _, format := range unmentionedFormats {
|
||||
if !config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
if !*config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
t.Errorf("Expected %s to be enabled (not mentioned in config), but it was disabled", format)
|
||||
}
|
||||
}
|
||||
@@ -155,13 +155,13 @@ integrations:
|
||||
}
|
||||
|
||||
for _, format := range expectedEnabled {
|
||||
if !config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
if !*config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
t.Errorf("Expected %s to be enabled, but it was disabled", format)
|
||||
}
|
||||
}
|
||||
|
||||
for _, format := range expectedDisabled {
|
||||
if config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
if *config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
t.Errorf("Expected %s to be disabled, but it was enabled", format)
|
||||
}
|
||||
}
|
||||
@@ -182,7 +182,7 @@ integrations:
|
||||
testVideoPreviewConfig(t, config, func(t *testing.T, config *Settings) {
|
||||
// All video formats should be enabled by default
|
||||
for _, format := range AllVideoPreviewTypes {
|
||||
if !config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
if !*config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
t.Errorf("Expected %s to be enabled by default, but it was disabled", format)
|
||||
}
|
||||
}
|
||||
@@ -217,7 +217,7 @@ integrations:
|
||||
for category, formats := range categories {
|
||||
t.Run(category, func(t *testing.T) {
|
||||
for _, format := range formats {
|
||||
if !config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
if !*config.Integrations.Media.Convert.VideoPreview[format] {
|
||||
t.Errorf("Expected %s (%s) to be enabled by default, but it was disabled", format, category)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package indexing
|
||||
package dbindex
|
||||
|
||||
import (
|
||||
"time"
|
||||
@@ -1,4 +1,4 @@
|
||||
package indexing
|
||||
package dbindex
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@@ -167,3 +167,37 @@ func (s *Storage) Flush() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetAllComplexities sets the complexity to 0 for all indexes and their scanners.
|
||||
// This should be called when the index database is recreated (either fresh start or after corruption).
|
||||
func (s *Storage) ResetAllComplexities() error {
|
||||
// Get all indexes from storage
|
||||
infos, err := s.All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset complexity for each index
|
||||
for _, info := range infos {
|
||||
if info == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Reset index-level complexity
|
||||
info.Complexity = 0
|
||||
|
||||
// Reset scanner-level complexities
|
||||
for _, scanner := range info.Scanners {
|
||||
if scanner != nil {
|
||||
scanner.Complexity = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Save the updated info
|
||||
if err := s.Save(info); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func createIndexDB(name string, journalMode string, lockingMode string, batchSiz
|
||||
BatchSize: batchSize,
|
||||
CacheSizeKB: cacheSizeKB, // From config, converted to KB
|
||||
SoftHeapLimitBytes: 16 * 1024 * 1024, // 16MB soft heap limit (reduced to minimize memory pressure)
|
||||
CacheSpillThreshold: 200, // Spill dirty pages to disk when cache exceeds 200 pages (~800KB) - more aggressive
|
||||
CacheSpillThreshold: 2000, // Spill dirty pages to disk when cache exceeds 2000 pages (~8MB)
|
||||
MmapSize: 0, // Disable mmap to prevent additional OS page cache usage
|
||||
Synchronous: "OFF", // No sync for maximum performance - safe since DB can be rebuilt
|
||||
TempStore: "FILE", // FILE instead of MEMORY to reduce memory usage
|
||||
@@ -61,33 +61,37 @@ func createIndexDB(name string, journalMode string, lockingMode string, batchSiz
|
||||
// batchSize: number of items per batch transaction
|
||||
// cacheSizeMB: cache size in megabytes
|
||||
// disableReuse: true to delete and recreate DB on startup, false to reuse existing (default)
|
||||
func NewIndexDB(name string, journalMode string, batchSize int, cacheSizeMB int, disableReuse bool) (*IndexDB, error) {
|
||||
// Returns the IndexDB and a boolean indicating if the database was recreated (true) or reused (false)
|
||||
func NewIndexDB(name string, journalMode string, batchSize int, cacheSizeMB int, disableReuse bool) (*IndexDB, bool, error) {
|
||||
wasRecreated := disableReuse // If disableReuse is true, we're creating fresh
|
||||
|
||||
idxDB, err := createIndexDB(name, journalMode, "", batchSize, cacheSizeMB, disableReuse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// For persistent databases, check integrity on startup
|
||||
// If corrupted, delete and recreate the database
|
||||
if !disableReuse {
|
||||
if err := idxDB.checkIntegrityAndRecreateIfNeeded(); err != nil {
|
||||
// Database was recreated, need to create a new connection
|
||||
// Database was recreated due to corruption
|
||||
wasRecreated = true
|
||||
idxDB.Close()
|
||||
|
||||
// Recreate the database connection
|
||||
idxDB, err = createIndexDB(name, journalMode, "", batchSize, cacheSizeMB, disableReuse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := idxDB.CreateIndexTable(); err != nil {
|
||||
idxDB.Close()
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
go idxDB.startPeriodicCleanup()
|
||||
return idxDB, nil
|
||||
return idxDB, wasRecreated, nil
|
||||
}
|
||||
|
||||
// checkIntegrityAndRecreateIfNeeded checks database integrity.
|
||||
|
||||
@@ -247,12 +247,22 @@ func NewTempDB(id string, config ...*TempDBConfig) (*TempDB, error) {
|
||||
db.SetMaxIdleConns(1)
|
||||
}
|
||||
|
||||
// Calculate cache_size in pages (negative value) to ensure consistent unit handling
|
||||
// SQLite's cache_size: positive = KB, negative = pages
|
||||
// When using positive KB values, SQLite seems to have issues with cache_spill being less than cache_size
|
||||
// So we convert to pages (negative) to ensure both cache_size and cache_spill use the same units
|
||||
cacheSizeInPages := cfg.CacheSizeKB
|
||||
if cfg.CacheSizeKB > 0 {
|
||||
// Convert KB to pages: KB / 4 = pages, then negate to indicate pages
|
||||
cacheSizeInPages = -(cfg.CacheSizeKB / 4)
|
||||
}
|
||||
|
||||
basePragmas := []struct {
|
||||
sql string
|
||||
err string
|
||||
}{
|
||||
{fmt.Sprintf("PRAGMA journal_mode = %s;", cfg.JournalMode), "failed to set journal_mode"},
|
||||
{fmt.Sprintf("PRAGMA cache_size = %d;", cfg.CacheSizeKB), "failed to set cache_size"},
|
||||
{fmt.Sprintf("PRAGMA cache_size = %d;", cacheSizeInPages), "failed to set cache_size"},
|
||||
{fmt.Sprintf("PRAGMA synchronous = %s;", cfg.Synchronous), "failed to set synchronous"},
|
||||
{fmt.Sprintf("PRAGMA temp_store = %s;", cfg.TempStore), "failed to set temp_store"},
|
||||
{fmt.Sprintf("PRAGMA locking_mode = %s;", cfg.LockingMode), "failed to set locking_mode"},
|
||||
@@ -274,19 +284,7 @@ func NewTempDB(id string, config ...*TempDBConfig) (*TempDB, error) {
|
||||
}{{fmt.Sprintf("PRAGMA hard_heap_limit = %d;", cfg.HardHeapLimitBytes), "failed to set hard_heap_limit"}}, basePragmas...)
|
||||
}
|
||||
|
||||
// Cache spill threshold - triggers early spilling to reduce memory usage
|
||||
if cfg.CacheSpillThreshold > 0 {
|
||||
basePragmas = append(basePragmas, struct {
|
||||
sql string
|
||||
err string
|
||||
}{fmt.Sprintf("PRAGMA cache_spill = %d;", cfg.CacheSpillThreshold), "failed to set cache_spill"})
|
||||
} else {
|
||||
// Enable cache_spill by default (value of 1 enables it)
|
||||
basePragmas = append(basePragmas, struct {
|
||||
sql string
|
||||
err string
|
||||
}{"PRAGMA cache_spill = 1;", "failed to set cache_spill"})
|
||||
}
|
||||
// Cache spill will be set later, after all other pragmas, to avoid interaction issues
|
||||
|
||||
// Page size must be set before any tables are created
|
||||
if cfg.PageSize > 0 {
|
||||
@@ -333,6 +331,32 @@ func NewTempDB(id string, config ...*TempDBConfig) (*TempDB, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Set cache_spill LAST, after all other pragmas are configured
|
||||
// SQLite may adjust cache_spill based on internal constraints or formulas
|
||||
if cfg.CacheSpillThreshold > 0 {
|
||||
// Try setting cache_spill multiple times with different approaches
|
||||
// Approach 1: Standard syntax
|
||||
if _, err := db.Exec(fmt.Sprintf("PRAGMA cache_spill = %d;", cfg.CacheSpillThreshold)); err != nil {
|
||||
logger.Warningf("[SQLITE_CONFIG] Failed to set cache_spill with standard syntax: %v", err)
|
||||
}
|
||||
|
||||
// Immediately verify what value was actually set
|
||||
var actualValue int64
|
||||
if err := db.QueryRow("PRAGMA cache_spill").Scan(&actualValue); err == nil {
|
||||
if actualValue != int64(cfg.CacheSpillThreshold) {
|
||||
// SQLite adjusted our value - calculate what percentage it used
|
||||
percentage := float64(actualValue) / float64(-cacheSizeInPages) * 100.0
|
||||
logger.Warningf("[SQLITE_CONFIG] SQLite adjusted cache_spill from %d to %d pages (%.1f%% of cache_size). This may be due to internal constraints.",
|
||||
cfg.CacheSpillThreshold, actualValue, percentage)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Enable cache_spill with default behavior
|
||||
if _, err := db.Exec("PRAGMA cache_spill = 1;"); err != nil {
|
||||
logger.Warningf("[SQLITE_CONFIG] Failed to enable cache_spill: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify PRAGMA settings were applied correctly
|
||||
if err := verifyPragmaSettings(db, cfg); err != nil {
|
||||
logger.Warningf("Failed to verify PRAGMA settings: %v", err)
|
||||
@@ -452,18 +476,39 @@ func verifyPragmaSettings(db *sql.DB, cfg *TempDBConfig) error {
|
||||
cacheSizeKB = -cacheSize * 4 // pages * 4KB per page
|
||||
}
|
||||
|
||||
// Expected cache_spill value in pages (cache_spill is always specified and returned in pages)
|
||||
expectedCacheSpill := int64(cfg.CacheSpillThreshold)
|
||||
|
||||
// Convert numeric PRAGMA values to human-readable strings
|
||||
tempStoreMap := map[string]string{"0": "DEFAULT", "1": "FILE", "2": "MEMORY"}
|
||||
synchronousMap := map[string]string{"0": "OFF", "1": "NORMAL", "2": "FULL", "3": "EXTRA"}
|
||||
autoVacuumMap := map[string]string{"0": "NONE", "1": "FULL", "2": "INCREMENTAL"}
|
||||
|
||||
tempStoreStr := tempStoreMap[tempStore]
|
||||
if tempStoreStr == "" {
|
||||
tempStoreStr = tempStore
|
||||
}
|
||||
synchronousStr := synchronousMap[synchronous]
|
||||
if synchronousStr == "" {
|
||||
synchronousStr = synchronous
|
||||
}
|
||||
autoVacuumStr := autoVacuumMap[autoVacuum]
|
||||
if autoVacuumStr == "" {
|
||||
autoVacuumStr = autoVacuum
|
||||
}
|
||||
|
||||
// Log all settings
|
||||
logger.Debugf("[SQLITE_CONFIG] Database PRAGMA settings verified:")
|
||||
logger.Debugf(" cache_size: %d KB (configured: %d KB)", cacheSizeKB, cfg.CacheSizeKB)
|
||||
logger.Debugf(" mmap_size: %d bytes (configured: %d bytes)", mmapSize, cfg.MmapSize)
|
||||
logger.Debugf(" soft_heap_limit: %d bytes (configured: %d bytes)", softHeapLimit, cfg.SoftHeapLimitBytes)
|
||||
logger.Debugf(" hard_heap_limit: %d bytes (configured: %d bytes)", hardHeapLimit, cfg.HardHeapLimitBytes)
|
||||
logger.Debugf(" cache_spill: %d pages (configured: %d pages)", cacheSpill, cfg.CacheSpillThreshold)
|
||||
logger.Debugf(" journal_mode: %s (configured: %s)", journalMode, cfg.JournalMode)
|
||||
logger.Debugf(" synchronous: %s (configured: %s)", synchronous, cfg.Synchronous)
|
||||
logger.Debugf(" temp_store: %s (configured: %s)", tempStore, cfg.TempStore)
|
||||
logger.Debugf(" locking_mode: %s (configured: %s)", lockingMode, cfg.LockingMode)
|
||||
logger.Debugf(" auto_vacuum: %s (configured: %s)", autoVacuum, cfg.AutoVacuum)
|
||||
logger.Debugf(" cache_size : %d KB", cacheSizeKB)
|
||||
logger.Debugf(" mmap_size : %d bytes", mmapSize)
|
||||
logger.Debugf(" soft_heap_limit : %d bytes", softHeapLimit)
|
||||
logger.Debugf(" hard_heap_limit : %d bytes", hardHeapLimit)
|
||||
logger.Debugf(" cache_spill : %d pages", cacheSpill)
|
||||
logger.Debugf(" journal_mode : %s", journalMode)
|
||||
logger.Debugf(" synchronous : %s", synchronousStr)
|
||||
logger.Debugf(" temp_store : %s", tempStoreStr)
|
||||
logger.Debugf(" locking_mode : %s", lockingMode)
|
||||
logger.Debugf(" auto_vacuum : %s", autoVacuumStr)
|
||||
|
||||
// Verify critical settings match
|
||||
if cfg.CacheSizeKB != 0 {
|
||||
@@ -484,6 +529,31 @@ func verifyPragmaSettings(db *sql.DB, cfg *TempDBConfig) error {
|
||||
if cfg.HardHeapLimitBytes > 0 && hardHeapLimit != cfg.HardHeapLimitBytes {
|
||||
logger.Warningf("[SQLITE_CONFIG] hard_heap_limit mismatch: got %d bytes, expected %d bytes", hardHeapLimit, cfg.HardHeapLimitBytes)
|
||||
}
|
||||
// Verify cache_spill matches expected value (accounting for unit conversion)
|
||||
if cfg.CacheSpillThreshold > 0 && cacheSpill != expectedCacheSpill {
|
||||
logger.Warningf("[SQLITE_CONFIG] cache_spill mismatch: got %d pages, expected %d pages (from threshold %d pages)", cacheSpill, expectedCacheSpill, cfg.CacheSpillThreshold)
|
||||
// If cache_spill equals cache_size in pages, it means SQLite ignored our setting
|
||||
var cacheSizePages int64
|
||||
if cfg.CacheSizeKB > 0 {
|
||||
cacheSizePages = int64(cfg.CacheSizeKB) / 4 // KB to pages
|
||||
} else {
|
||||
cacheSizePages = -int64(cfg.CacheSizeKB) // Already in pages (negative)
|
||||
}
|
||||
if cacheSpill == cacheSizePages {
|
||||
logger.Warningf("[SQLITE_CONFIG] cache_spill equals cache_size (%d pages), indicating SQLite may have ignored our setting", cacheSizePages)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify string-based PRAGMA settings match (using converted values)
|
||||
if synchronousStr != cfg.Synchronous {
|
||||
logger.Warningf("[SQLITE_CONFIG] synchronous mismatch: got %s, expected %s", synchronousStr, cfg.Synchronous)
|
||||
}
|
||||
if tempStoreStr != cfg.TempStore {
|
||||
logger.Warningf("[SQLITE_CONFIG] temp_store mismatch: got %s, expected %s", tempStoreStr, cfg.TempStore)
|
||||
}
|
||||
if autoVacuumStr != cfg.AutoVacuum {
|
||||
logger.Warningf("[SQLITE_CONFIG] auto_vacuum mismatch: got %s, expected %s", autoVacuumStr, cfg.AutoVacuum)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/dbindex"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
)
|
||||
@@ -19,7 +19,7 @@ type BoltStore struct {
|
||||
Auth *auth.Storage
|
||||
Settings *settings.Storage
|
||||
Access *access.Storage
|
||||
Indexing *indexing.Storage
|
||||
Indexing *dbindex.Storage
|
||||
}
|
||||
|
||||
// NewStorage creates a storage.Storage based on Bolt DB.
|
||||
@@ -35,6 +35,6 @@ func NewStorage(db *storm.DB) (*BoltStore, error) {
|
||||
Auth: authStore,
|
||||
Settings: settings.NewStorage(settingsBackend{db: db}),
|
||||
Access: access.NewStorage(db, userStore),
|
||||
Indexing: indexing.NewStorage(indexingBackend{db: db}),
|
||||
Indexing: dbindex.NewStorage(indexingBackend{db: db}),
|
||||
}, nil
|
||||
}
|
||||
@@ -5,15 +5,15 @@ import (
|
||||
"github.com/asdine/storm/v3/q"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/dbindex"
|
||||
)
|
||||
|
||||
type indexingBackend struct {
|
||||
db *storm.DB
|
||||
}
|
||||
|
||||
func (s indexingBackend) All() ([]*indexing.IndexInfo, error) {
|
||||
var v []*indexing.IndexInfo
|
||||
func (s indexingBackend) All() ([]*dbindex.IndexInfo, error) {
|
||||
var v []*dbindex.IndexInfo
|
||||
err := s.db.All(&v)
|
||||
if err == storm.ErrNotFound {
|
||||
return v, errors.ErrNotExist
|
||||
@@ -22,8 +22,8 @@ func (s indexingBackend) All() ([]*indexing.IndexInfo, error) {
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (s indexingBackend) GetByPath(path string) (*indexing.IndexInfo, error) {
|
||||
var v indexing.IndexInfo
|
||||
func (s indexingBackend) GetByPath(path string) (*dbindex.IndexInfo, error) {
|
||||
var v dbindex.IndexInfo
|
||||
err := s.db.One("Path", path, &v)
|
||||
if err == storm.ErrNotFound {
|
||||
return nil, errors.ErrNotExist
|
||||
@@ -32,8 +32,8 @@ func (s indexingBackend) GetByPath(path string) (*indexing.IndexInfo, error) {
|
||||
return &v, err
|
||||
}
|
||||
|
||||
func (s indexingBackend) GetBySource(source string) ([]*indexing.IndexInfo, error) {
|
||||
var v []*indexing.IndexInfo
|
||||
func (s indexingBackend) GetBySource(source string) ([]*dbindex.IndexInfo, error) {
|
||||
var v []*dbindex.IndexInfo
|
||||
err := s.db.Select(q.Eq("Source", source)).Find(&v)
|
||||
if err == storm.ErrNotFound {
|
||||
return nil, errors.ErrNotExist
|
||||
@@ -41,12 +41,12 @@ func (s indexingBackend) GetBySource(source string) ([]*indexing.IndexInfo, erro
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (s indexingBackend) Save(info *indexing.IndexInfo) error {
|
||||
func (s indexingBackend) Save(info *dbindex.IndexInfo) error {
|
||||
return s.db.Save(info)
|
||||
}
|
||||
|
||||
func (s indexingBackend) Delete(path string) error {
|
||||
err := s.db.DeleteStruct(&indexing.IndexInfo{Path: path})
|
||||
err := s.db.DeleteStruct(&dbindex.IndexInfo{Path: path})
|
||||
if err == storm.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@ type NonAdminEditable struct {
|
||||
ShowSelectMultiple bool `json:"showSelectMultiple"` // show select multiple files on desktop
|
||||
DebugOffice bool `json:"debugOffice"` // debug onlyoffice editor
|
||||
OtpEnabled bool `json:"otpEnabled"` // allow non-admin users to disable their own OTP
|
||||
DefaultLandingPage string `json:"defaultLandingPage"` // deprecated: determined by sidebar link order instead
|
||||
SidebarLinks []SidebarLink `json:"sidebarLinks"` // customizable sidebar links
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,29 @@
|
||||
package ffmpeg
|
||||
|
||||
import "github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
|
||||
func CanConvertImage(ext string) bool {
|
||||
if !MediaEnabled() {
|
||||
return false
|
||||
}
|
||||
val := settings.Config.Integrations.Media.Convert.ImagePreview[settings.ImagePreviewType(ext)]
|
||||
if val == nil {
|
||||
return false // Extension not in the configured list
|
||||
}
|
||||
return *val
|
||||
}
|
||||
|
||||
func CanConvertVideo(ext string) bool {
|
||||
if !MediaEnabled() {
|
||||
return false
|
||||
}
|
||||
val := settings.Config.Integrations.Media.Convert.VideoPreview[settings.VideoPreviewType(ext)]
|
||||
if val == nil {
|
||||
return false // Extension not in the configured list
|
||||
}
|
||||
return *val
|
||||
}
|
||||
|
||||
func MediaEnabled() bool {
|
||||
return settings.Env.FFmpegPath != "" && settings.Env.FFprobePath != ""
|
||||
}
|
||||
@@ -378,3 +378,11 @@ func (s *FFmpegService) ConvertHEICToJPEG(ctx context.Context, heicPath string,
|
||||
|
||||
return jpegBytes, nil
|
||||
}
|
||||
|
||||
// ConvertImageToJPEG converts any image format (including problematic JPEGs) to standard JPEG using FFmpeg
|
||||
// This handles extended JPEG formats that Go's image/jpeg decoder doesn't support (e.g., SOF1/C1 marker)
|
||||
// and can be used as a fallback for images that fail to decode with Go's standard library.
|
||||
func (s *FFmpegService) ConvertImageToJPEG(ctx context.Context, imagePath string, targetWidth, targetHeight int, quality string) ([]byte, error) {
|
||||
// Use the direct conversion method which works for all image formats
|
||||
return s.ConvertHEICToJPEGDirect(ctx, imagePath, targetWidth, targetHeight, quality)
|
||||
}
|
||||
+19
-19
@@ -9,28 +9,28 @@ require (
|
||||
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.29.0
|
||||
github.com/goccy/go-yaml v1.19.0
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gtsteffaniak/go-cache v1.1.0
|
||||
github.com/gtsteffaniak/go-logger v1.0.2
|
||||
github.com/gtsteffaniak/go-push v0.1.0
|
||||
github.com/kovidgoyal/imaging v1.7.2
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/kovidgoyal/imaging v1.8.19
|
||||
github.com/mattn/go-sqlite3 v1.14.33
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/image v0.34.0
|
||||
golang.org/x/mod v0.31.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/image v0.35.0
|
||||
golang.org/x/mod v0.32.0
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/time v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.40.1
|
||||
modernc.org/sqlite v1.44.3
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -54,7 +54,7 @@ require (
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/MirrexOne/unqueryvet v1.4.0 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||
github.com/air-verse/air v1.64.0 // indirect
|
||||
github.com/air-verse/air v1.64.2 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.21.1 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
||||
@@ -111,7 +111,7 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/spec v0.22.2 // indirect
|
||||
github.com/go-openapi/spec v0.22.3 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
@@ -134,7 +134,7 @@ require (
|
||||
github.com/godoc-lint/godoc-lint v0.11.1 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/gohugoio/hugo v0.149.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20251215195340-f7ee789d9fc1 // indirect
|
||||
github.com/golang/geo v0.0.0-20260120070133-792bb8583fbb // 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
|
||||
@@ -162,12 +162,14 @@ 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/joho/godotenv v1.5.1 // indirect
|
||||
github.com/julz/importas v0.2.0 // indirect
|
||||
github.com/jupiterrider/ffi v0.5.1 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
|
||||
github.com/kisielk/errcheck v1.9.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
|
||||
github.com/kovidgoyal/go-shm v1.0.0 // indirect
|
||||
github.com/kulti/thelper v0.7.1 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.15 // indirect
|
||||
github.com/lasiar/canonicalheader v1.1.2 // indirect
|
||||
@@ -205,7 +207,6 @@ require (
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/polyfloyd/go-errorlint v1.8.0 // indirect
|
||||
github.com/prometheus/client_golang v1.12.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
@@ -268,17 +269,16 @@ require (
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
honnef.co/go/tools v0.6.1 // indirect
|
||||
modernc.org/libc v1.67.1 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
mvdan.cc/gofumpt v0.9.2 // indirect
|
||||
|
||||
+40
-90
@@ -62,8 +62,6 @@ github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
@@ -75,35 +73,22 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/MirrexOne/unqueryvet v1.2.1 h1:M+zdXMq84g+E1YOLa7g7ExN3dWfZQrdDSTCM7gC+m/A=
|
||||
github.com/MirrexOne/unqueryvet v1.2.1/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg=
|
||||
github.com/MirrexOne/unqueryvet v1.3.0 h1:5slWSomgqpYU4zFuZ3NNOfOUxVPlXFDBPAVasZOGlAY=
|
||||
github.com/MirrexOne/unqueryvet v1.3.0/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg=
|
||||
github.com/MirrexOne/unqueryvet v1.4.0 h1:6KAkqqW2KUnkl9Z0VuTphC3IXRPoFqEkJEtyxxHj5eQ=
|
||||
github.com/MirrexOne/unqueryvet v1.4.0/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/air-verse/air v1.63.1 h1:N6kD5niKKVx0wF2mW0mgK6LNfJqP5/lCAqm3WWl9vlw=
|
||||
github.com/air-verse/air v1.63.1/go.mod h1:Dnn4m4DlC9IQiNd3ir57SOdpvGJ3gnC1+OlIGMi2fJY=
|
||||
github.com/air-verse/air v1.63.4 h1:Z+R4328Bja5QKFMTP0CNeT8aVWdb3D5kbbFvnXnuRhE=
|
||||
github.com/air-verse/air v1.63.4/go.mod h1:Dnn4m4DlC9IQiNd3ir57SOdpvGJ3gnC1+OlIGMi2fJY=
|
||||
github.com/air-verse/air v1.63.7 h1:Wi85jUmyhGWcNpm/OUUvPiGNp65X+mUVKjr1N9HfZ3Q=
|
||||
github.com/air-verse/air v1.63.7/go.mod h1:Dnn4m4DlC9IQiNd3ir57SOdpvGJ3gnC1+OlIGMi2fJY=
|
||||
github.com/air-verse/air v1.64.0 h1:lKEvIQzaOM5vAVKOdQxq1WrXLfLmzz+e+hPrhmgF6yo=
|
||||
github.com/air-verse/air v1.64.0/go.mod h1:Dnn4m4DlC9IQiNd3ir57SOdpvGJ3gnC1+OlIGMi2fJY=
|
||||
github.com/air-verse/air v1.64.2 h1:H49xKRa4r3uFUycmCCA+JectVuZLNMPGdypLFpoHVcY=
|
||||
github.com/air-verse/air v1.64.2/go.mod h1:SOwv08NTvKextanAY819AK0FK6XzQO5/tC6s1R+rk8o=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||
github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA=
|
||||
github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
|
||||
github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
|
||||
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -111,8 +96,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
|
||||
github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
|
||||
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
|
||||
github.com/alexkohler/prealloc v1.0.1 h1:A9P1haqowqUxWvU9nk6tQ7YktXIHf+LQM9wPRhuteEE=
|
||||
github.com/alexkohler/prealloc v1.0.1/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
|
||||
github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
|
||||
@@ -184,8 +167,6 @@ github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E
|
||||
github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70=
|
||||
github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=
|
||||
github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=
|
||||
github.com/catenacyber/perfsprint v0.10.0 h1:AZj1mYyxbxLRqmnYOeguZXEQwWOgQGm2wzLI5d7Hl/0=
|
||||
github.com/catenacyber/perfsprint v0.10.0/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
|
||||
github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=
|
||||
github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
|
||||
@@ -292,12 +273,8 @@ github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghostiam/protogetter v0.3.17 h1:sjGPErP9o7i2Ym+z3LsQzBdLCNaqbYy2iJQPxGXg04Q=
|
||||
github.com/ghostiam/protogetter v0.3.17/go.mod h1:AivIX1eKA/TcUmzZdzbl+Tb8tjIe8FcyG6JFyemQAH4=
|
||||
github.com/ghostiam/protogetter v0.3.18 h1:yEpghRGtP9PjKvVXtEzGpYfQj1Wl/ZehAfU6fr62Lfo=
|
||||
github.com/ghostiam/protogetter v0.3.18/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
|
||||
github.com/go-critic/go-critic v0.14.2 h1:PMvP5f+LdR8p6B29npvChUXbD1vrNlKDf60NJtgMBOo=
|
||||
github.com/go-critic/go-critic v0.14.2/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
|
||||
github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
|
||||
github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
@@ -323,8 +300,8 @@ github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegO
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||
github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc=
|
||||
github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
|
||||
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
|
||||
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
@@ -352,8 +329,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.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
|
||||
github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
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=
|
||||
@@ -386,12 +363,8 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4
|
||||
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
|
||||
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godoc-lint/godoc-lint v0.10.1 h1:ZPUVzlDtJfA+P688JfPJPkI/SuzcBr/753yGIk5bOPA=
|
||||
github.com/godoc-lint/godoc-lint v0.10.1/go.mod h1:KleLcHu/CGSvkjUH2RvZyoK1MBC7pDQg4NxMYLcBBsw=
|
||||
github.com/godoc-lint/godoc-lint v0.10.2 h1:dksNgK+zebnVlj4Fx83CRnCmPO0qRat/9xfFsir1nfg=
|
||||
github.com/godoc-lint/godoc-lint v0.10.2/go.mod h1:KleLcHu/CGSvkjUH2RvZyoK1MBC7pDQg4NxMYLcBBsw=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godoc-lint/godoc-lint v0.11.1 h1:z9as8Qjiy6miRIa3VRymTa+Gt2RLnGICVikcvlUVOaA=
|
||||
github.com/godoc-lint/godoc-lint v0.11.1/go.mod h1:BAqayheFSuZrEAqCRxgw9MyvsM+S/hZwJbU1s/ejRj8=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
@@ -418,8 +391,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20251215195340-f7ee789d9fc1 h1:D/2kgdp+Hl79R59gNSnTQPpjWKd/IVUYhuvkqWQG8N0=
|
||||
github.com/golang/geo v0.0.0-20251215195340-f7ee789d9fc1/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
||||
github.com/golang/geo v0.0.0-20260120070133-792bb8583fbb h1:OhyJ/wXEqRssvvFcXP8Wzoyn0fbiqWgjHXLKj3ZOU+4=
|
||||
github.com/golang/geo v0.0.0-20260120070133-792bb8583fbb/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -459,14 +432,8 @@ github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarog
|
||||
github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
|
||||
github.com/golangci/golangci-lint/v2 v2.6.2 h1:jkMSVv36JmyTENcEertckvimvjPcD5qxNM7W7qhECvI=
|
||||
github.com/golangci/golangci-lint/v2 v2.6.2/go.mod h1:fSIMDiBt9kzdpnvvV7GO6iWzyv5uaeZ+iPor+2uRczE=
|
||||
github.com/golangci/golangci-lint/v2 v2.7.2 h1:AhBC+YeEueec4AGlIbvPym5C70Thx0JykIqXbdIXWx0=
|
||||
github.com/golangci/golangci-lint/v2 v2.7.2/go.mod h1:pDijleoBu7e8sejMqyZ3L5n6geqe+cVvOAz2QImqqVc=
|
||||
github.com/golangci/golangci-lint/v2 v2.8.0 h1:wJnr3hJWY3eVzOUcfwbDc2qbi2RDEpvLmQeNFaPSNYA=
|
||||
github.com/golangci/golangci-lint/v2 v2.8.0/go.mod h1:xl+HafQ9xoP8rzw0z5AwnO5kynxtb80e8u02Ej/47RI=
|
||||
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8=
|
||||
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ=
|
||||
github.com/golangci/golines v0.14.0 h1:xt9d3RKBjhasA3qpoXs99J2xN2t6eBlpLHt0TrgyyXc=
|
||||
github.com/golangci/golines v0.14.0/go.mod h1:gf555vPG2Ia7mmy2mzmhVQbVjuK8Orw0maR1G4vVAAQ=
|
||||
github.com/golangci/misspell v0.7.0 h1:4GOHr/T1lTW0hhR4tgaaV1WS/lJ+ncvYCoFKmqJsj0c=
|
||||
@@ -531,8 +498,6 @@ github.com/gtsteffaniak/go-cache v1.1.0 h1:ni/2cJpCxpGmaMZd92iTDIGOy+yUcMcxQuW7b
|
||||
github.com/gtsteffaniak/go-cache v1.1.0/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
|
||||
github.com/gtsteffaniak/go-logger v1.0.2 h1:S4AgR7yDu2geD8y2D9NyJVOBqpZRnl37r2mBi7GDNR0=
|
||||
github.com/gtsteffaniak/go-logger v1.0.2/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
|
||||
github.com/gtsteffaniak/go-push v0.1.0 h1:bMkin7Ce2xjTllPMk5q3IAxD2ZcMDGZICuQeeKDJDXo=
|
||||
github.com/gtsteffaniak/go-push v0.1.0/go.mod h1:xZplgyS4bJdYmLbfEBbkETdrN/vQn6M/tim72fX2s1c=
|
||||
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=
|
||||
@@ -540,8 +505,6 @@ github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1T
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@@ -565,6 +528,8 @@ github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjz
|
||||
github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=
|
||||
github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
|
||||
github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
@@ -591,8 +556,10 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok=
|
||||
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
|
||||
github.com/kovidgoyal/imaging v1.7.2 h1:mmT6k6Az3mC6dbqdZ6Q9KQCdZFWTAQ+q97NyGZgJ/2c=
|
||||
github.com/kovidgoyal/imaging v1.7.2/go.mod h1:GdkCORjfZMMGFY0Pb7TDmRhj7PDhxF/QShKukSCj0VU=
|
||||
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
|
||||
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
|
||||
github.com/kovidgoyal/imaging v1.8.19 h1:zWJdQqF2tfSKjvoB7XpLRhVGbYsze++M0iaqZ4ZkhNk=
|
||||
github.com/kovidgoyal/imaging v1.8.19/go.mod h1:I0q8RdoEuyc4G8GFOF9CaluTUHQSf68d6TmsqpvfRI8=
|
||||
github.com/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=
|
||||
@@ -611,8 +578,6 @@ github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0
|
||||
github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=
|
||||
github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=
|
||||
github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=
|
||||
github.com/ldez/gomoddirectives v0.7.1 h1:FaULkvUIG36hj6chpwa+FdCNGZBsD7/fO+p7CCsM6pE=
|
||||
github.com/ldez/gomoddirectives v0.7.1/go.mod h1:auDNtakWJR1rC+YX7ar+HmveqXATBAyEK1KYpsIRW/8=
|
||||
github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk=
|
||||
github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q=
|
||||
github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
|
||||
@@ -657,12 +622,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgechev/revive v1.12.0 h1:Q+/kkbbwerrVYPv9d9efaPGmAO/NsxwW/nE6ahpQaCU=
|
||||
github.com/mgechev/revive v1.12.0/go.mod h1:VXsY2LsTigk8XU9BpZauVLjVrhICMOV3k1lpB3CXrp8=
|
||||
github.com/mgechev/revive v1.13.0 h1:yFbEVliCVKRXY8UgwEO7EOYNopvjb1BFbmYqm9hZjBM=
|
||||
github.com/mgechev/revive v1.13.0/go.mod h1:efJfeBVCX2JUumNQ7dtOLDja+QKj9mYGgEZA7rt5u+0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
@@ -708,9 +671,8 @@ github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE=
|
||||
github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
|
||||
@@ -734,8 +696,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polyfloyd/go-errorlint v1.8.0 h1:DL4RestQqRLr8U4LygLw8g2DX6RN1eBJOpa2mzsrl1Q=
|
||||
github.com/polyfloyd/go-errorlint v1.8.0/go.mod h1:G2W0Q5roxbLCt0ZQbdoxQxXktTjwNyDbEaj3n7jvl4s=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
@@ -799,10 +759,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.10 h1:ntbBqdWXnu46DUOXn+R2SvPo3PiJCDugTCgTW2g4tQg=
|
||||
github.com/securego/gosec/v2 v2.22.10/go.mod h1:9UNjK3tLpv/w2b0+7r82byV43wCJDNtEDQMeS+H/g2w=
|
||||
github.com/securego/gosec/v2 v2.22.11-0.20251204091113-daccba6b93d7 h1:rZg6IGn0ySYZwCX8LHwZoYm03JhG/cVAJJ3O+u3Vclo=
|
||||
github.com/securego/gosec/v2 v2.22.11-0.20251204091113-daccba6b93d7/go.mod h1:9sr22NZO5Kfh7unW/xZxkGYTmj2484/fCiE54gw7UTY=
|
||||
github.com/securego/gosec/v2 v2.22.11 h1:tW+weM/hCM/GX3iaCV91d5I6hqaRT2TPsFM1+USPXwg=
|
||||
github.com/securego/gosec/v2 v2.22.11/go.mod h1:KE4MW/eH0GLWztkbt4/7XpyH0zJBBnu7sYB4l6Wn7Mw=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
@@ -825,8 +781,6 @@ 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.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
@@ -839,8 +793,6 @@ github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
|
||||
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -879,8 +831,6 @@ github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk
|
||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
|
||||
github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=
|
||||
github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.11.0 h1:BJSt36snX9+4WTIXeJ7nvHBQBcm1h2SjQMSlmQ6aFSU=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.11.0/go.mod h1:wFL9pDWDAbXhhPZZt+nG8Fu+h29TtnZ2MW6Lx4BRXIU=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
|
||||
@@ -959,8 +909,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -971,16 +921,16 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 h1:HDjDiATsGqvuqvkDvgJjD1IgPrVekcSXVVE21JwvzGE=
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -1006,8 +956,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1050,8 +1000,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1130,8 +1080,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -1148,8 +1098,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1205,8 +1155,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
@@ -1336,8 +1286,8 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
|
||||
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -1346,8 +1296,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
|
||||
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
||||
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -255,17 +255,13 @@ func buildOnlyOfficeDownloadURL(r *http.Request, source, path, hash, token strin
|
||||
baseURL = fmt.Sprintf("%s://%s%s", scheme, host, settings.Config.Server.BaseURL)
|
||||
}
|
||||
|
||||
var downloadURL string
|
||||
escapedPath := url.QueryEscape(path)
|
||||
downloadURL := fmt.Sprintf("%s/api/raw?file=%s&auth=%s",
|
||||
strings.TrimSuffix(baseURL, "/"), escapedPath, token)
|
||||
if hash != "" {
|
||||
// Share download URL - don't expose source name, just use the path relative to share
|
||||
filesParam := url.QueryEscape(path)
|
||||
downloadURL = fmt.Sprintf("%s/public/api/raw?files=%s&hash=%s&token=%s&auth=%s",
|
||||
strings.TrimSuffix(baseURL, "/"), filesParam, hash, token, token)
|
||||
downloadURL = downloadURL + "&hash=" + hash
|
||||
} else {
|
||||
// Regular download URL - include source for non-share requests
|
||||
filesParam := url.QueryEscape(source + "::" + path)
|
||||
downloadURL = fmt.Sprintf("%s/api/raw?files=%s&auth=%s",
|
||||
strings.TrimSuffix(baseURL, "/"), filesParam, token)
|
||||
downloadURL = downloadURL + "&source=" + url.QueryEscape(source)
|
||||
}
|
||||
|
||||
return downloadURL
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -79,7 +80,7 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
d.fileInfo = *fileInfo
|
||||
status, err := previewHelperFunc(w, r, d)
|
||||
if err != nil {
|
||||
logger.Errorf("error getting preview: %v", err)
|
||||
// Error already logged in previewHelperFunc or its callees
|
||||
return errToStatus(err), err
|
||||
}
|
||||
return status, nil
|
||||
@@ -201,7 +202,7 @@ func previewHelperFunc(w http.ResponseWriter, r *http.Request, d *requestContext
|
||||
|
||||
officeUrl := ""
|
||||
if d.fileInfo.OnlyOfficeId != "" {
|
||||
pathUrl := fmt.Sprintf("/api/raw?files=%s::%s", d.fileInfo.Source, d.fileInfo.Path)
|
||||
pathUrl := fmt.Sprintf("/api/raw?file=%s&source=%s", url.QueryEscape(d.fileInfo.Path), url.QueryEscape(d.fileInfo.Source))
|
||||
pathUrl = pathUrl + "&auth=" + d.token
|
||||
if settings.Config.Server.InternalUrl != "" {
|
||||
officeUrl = config.Server.InternalUrl + pathUrl
|
||||
|
||||
+19
-12
@@ -22,11 +22,15 @@ import (
|
||||
// publicRawHandler serves the raw content of a file, multiple files, or directory via a public share.
|
||||
// @Summary Download files from a public share
|
||||
// @Description Downloads raw content from a public share. Supports single files, multiple files, or directories as archives. Enforces download limits (global or per-user) and blocks anonymous users when per-user limits are enabled.
|
||||
// @Description
|
||||
// @Description **Multiple Files:**
|
||||
// @Description - Use repeated query parameters: `?file=file1.txt&file=file2.txt&file=file3.txt`
|
||||
// @Description - This supports filenames containing commas and special characters
|
||||
// @Tags Public Shares
|
||||
// @Accept json
|
||||
// @Produce octet-stream
|
||||
// @Param hash query string true "Share hash for authentication"
|
||||
// @Param files query string true "Comma-separated list of file paths. Example: 'file1.txt,folder/file2.txt'"
|
||||
// @Param file query []string true "File path (can be repeated for multiple files)"
|
||||
// @Param inline query bool false "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'."
|
||||
// @Param algo query string false "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'."
|
||||
// @Success 200 {file} file "Raw file or directory content, or archive for multiple files"
|
||||
@@ -71,12 +75,11 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if d.share.PerUserDownloadLimit {
|
||||
d.share.IncrementUserDownload(d.user.Username)
|
||||
}
|
||||
encodedFiles := r.URL.Query().Get("files")
|
||||
|
||||
// Decode the URL-encoded path - use PathUnescape to preserve + as literal character
|
||||
f, err := url.PathUnescape(encodedFiles)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
// Get all "file" parameter values (supports repeated params)
|
||||
encodedFiles := r.URL.Query()["file"]
|
||||
if len(encodedFiles) == 0 {
|
||||
return http.StatusBadRequest, fmt.Errorf("no files specified")
|
||||
}
|
||||
|
||||
// Get the actual source name from the share's source mapping
|
||||
@@ -86,22 +89,26 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
actualSourceName := sourceInfo.Name
|
||||
|
||||
// Parse comma-separated file list
|
||||
// Decode and process each file path
|
||||
fileList := []string{}
|
||||
for _, file := range strings.Split(f, ",") {
|
||||
for _, encodedFile := range encodedFiles {
|
||||
// Decode the URL-encoded path - use PathUnescape to preserve + as literal character
|
||||
file, decodeErr := url.PathUnescape(encodedFile)
|
||||
if decodeErr != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", decodeErr)
|
||||
}
|
||||
// Join the share path with the requested path
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, file)
|
||||
fileList = append(fileList, filePath)
|
||||
}
|
||||
|
||||
var status int
|
||||
status, err = rawFilesHandler(w, r, d, actualSourceName, fileList)
|
||||
status, err := rawFilesHandler(w, r, d, actualSourceName, 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)
|
||||
logger.Errorf("public share handler: error processing filelist: %v with error %v", encodedFiles, err)
|
||||
return status, fmt.Errorf("error processing filelist: %v", encodedFiles)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
+6
-3
@@ -94,10 +94,14 @@ func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName stri
|
||||
// @Description - The Content-Disposition header will always include both:
|
||||
// @Description 1. `filename="..."`: An ASCII-safe version of the filename for compatibility.
|
||||
// @Description 2. `filename*=utf-8"...`: The full UTF-8 encoded filename (RFC 6266/5987) for modern clients.
|
||||
// @Description
|
||||
// @Description **Multiple Files:**
|
||||
// @Description - Use repeated query parameters: `?file=file1.txt&file=file2.txt&file=file3.txt`
|
||||
// @Description - This supports filenames containing commas and special characters
|
||||
// @Tags Resources
|
||||
// @Accept json
|
||||
// @Param source query string true "Source name for the files (required)"
|
||||
// @Param files query string true "Comma-separated list of file paths (required)"
|
||||
// @Param file query []string true "File path (can be repeated for multiple files)"
|
||||
// @Param inline query bool false "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'."
|
||||
// @Param algo query string false "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'."
|
||||
// @Success 200 {file} file "Raw file or directory content, or archive for multiple files"
|
||||
@@ -108,8 +112,7 @@ func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName stri
|
||||
// @Router /api/raw [get]
|
||||
func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
source := r.URL.Query().Get("source")
|
||||
filesParam := r.URL.Query().Get("files")
|
||||
fileList := strings.Split(filesParam, ",")
|
||||
fileList := r.URL.Query()["file"]
|
||||
return rawFilesHandler(w, r, d, source, fileList)
|
||||
}
|
||||
|
||||
|
||||
@@ -793,7 +793,9 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
}
|
||||
|
||||
// Get real paths
|
||||
realSrc, isSrcDir, err := srcIdx.GetRealPath(userscopeSrc, item.FromPath)
|
||||
// Combine user scope with item paths BEFORE calling GetRealPath to avoid double scope application
|
||||
fullSrcPath := utils.JoinPathAsUnix(userscopeSrc, item.FromPath)
|
||||
realSrc, isSrcDir, err := srcIdx.GetRealPath(fullSrcPath)
|
||||
if err != nil {
|
||||
logger.Errorf("could not resolve source path: %v, item.FromPath: %v", err, item.FromPath)
|
||||
item.Message = "could not resolve source path"
|
||||
@@ -808,7 +810,9 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
}
|
||||
|
||||
// Check destination parent directory exists
|
||||
parentDir, _, err := dstIdx.GetRealPath(userscopeDst, filepath.Dir(item.ToPath))
|
||||
dstParentPath := filepath.Dir(item.ToPath)
|
||||
fullDstParentPath := utils.JoinPathAsUnix(userscopeDst, dstParentPath)
|
||||
parentDir, _, err := dstIdx.GetRealPath(fullDstParentPath)
|
||||
if err != nil {
|
||||
item.Message = "destination directory does not exist"
|
||||
if d.share != nil {
|
||||
@@ -938,6 +942,13 @@ func patchAction(ctx context.Context, params patchActionParams) error {
|
||||
case "rename", "move":
|
||||
idx := indexing.GetIndex(params.srcIndex)
|
||||
srcPath := idx.MakeIndexPath(params.src, params.isSrcDir)
|
||||
userScope := ""
|
||||
userScope, _ = params.d.user.GetScopeForSourceName(params.srcIndex)
|
||||
if userScope != "" && userScope != "/" {
|
||||
// Strip the user scope from srcPath so FileInfoFaster doesn't double it
|
||||
srcPath = strings.TrimPrefix(srcPath, userScope)
|
||||
}
|
||||
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
FollowSymlinks: true,
|
||||
Path: srcPath,
|
||||
|
||||
@@ -197,7 +197,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
"updateAvailable": utils.GetUpdateAvailableUrl(),
|
||||
"disableNavButtons": disableNavButtons,
|
||||
"userSelectableThemes": config.Frontend.Styling.CustomThemeOptions,
|
||||
"enableHeicConversion": config.Integrations.Media.Convert.ImagePreview[settings.HEICImagePreview] && settings.MediaEnabled(),
|
||||
"enableHeicConversion": settings.CanConvertImage("heic"),
|
||||
"eventBasedThemes": !config.Frontend.Styling.DisableEventBasedThemes,
|
||||
"loginIcon": loginIcon,
|
||||
"disableSidebar": disableSidebar,
|
||||
|
||||
@@ -17,7 +17,7 @@ func setupTestIndexForDeletion(t *testing.T) *Index {
|
||||
// Initialize the database if not already done
|
||||
if indexDB == nil {
|
||||
var err error
|
||||
indexDB, err = dbsql.NewIndexDB("test_deletion", "OFF", 1000, 32, false)
|
||||
indexDB, _, err = dbsql.NewIndexDB("test_deletion", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
indexingdb "github.com/gtsteffaniak/filebrowser/backend/database/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/dbindex"
|
||||
dbsql "github.com/gtsteffaniak/filebrowser/backend/database/sql"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
@@ -118,8 +118,8 @@ type Index struct {
|
||||
var (
|
||||
indexes map[string]*Index
|
||||
indexesMutex sync.RWMutex
|
||||
indexDB *dbsql.IndexDB // Shared database for all indexes
|
||||
indexingStorage *indexingdb.Storage // Persistent storage for index metadata
|
||||
indexDB *dbsql.IndexDB // Shared database for all indexes
|
||||
indexingStorage *dbindex.Storage // Persistent storage for index metadata
|
||||
)
|
||||
|
||||
type IndexStatus string
|
||||
@@ -321,7 +321,8 @@ func (idx *Index) getStatusUnlocked() IndexStatus {
|
||||
}
|
||||
|
||||
// InitializeIndexDB creates the shared index database for all sources.
|
||||
func InitializeIndexDB() error {
|
||||
// Returns true if the database was recreated (either fresh or after corruption).
|
||||
func InitializeIndexDB() (bool, error) {
|
||||
var err error
|
||||
journalMode := "OFF"
|
||||
if settings.Config.Server.IndexSqlConfig.WalMode {
|
||||
@@ -330,13 +331,18 @@ func InitializeIndexDB() error {
|
||||
batchSize := settings.Config.Server.IndexSqlConfig.BatchSize
|
||||
cacheSizeMB := settings.Config.Server.IndexSqlConfig.CacheSizeMB
|
||||
disableReuse := settings.Config.Server.IndexSqlConfig.DisableReuse
|
||||
indexDB, err = dbsql.NewIndexDB("all", journalMode, batchSize, cacheSizeMB, disableReuse)
|
||||
var wasRecreated bool
|
||||
indexDB, wasRecreated, err = dbsql.NewIndexDB("all", journalMode, batchSize, cacheSizeMB, disableReuse)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to initialize index database: %v", err)
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
return nil
|
||||
if wasRecreated {
|
||||
logger.Warningf("Index database was recreated, all index complexities will be reset to 0")
|
||||
}
|
||||
|
||||
return wasRecreated, nil
|
||||
}
|
||||
|
||||
// GetIndexDB returns the shared index database.
|
||||
@@ -364,7 +370,7 @@ func SetIndexDBForTesting(db *dbsql.IndexDB) {
|
||||
}
|
||||
|
||||
// SetIndexingStorage sets the persistent storage for index metadata.
|
||||
func SetIndexingStorage(storage *indexingdb.Storage) {
|
||||
func SetIndexingStorage(storage *dbindex.Storage) {
|
||||
indexingStorage = storage
|
||||
}
|
||||
|
||||
@@ -652,12 +658,12 @@ func (idx *Index) GetFsInfoCore(indexPath string, opts Options) (*iteminfo.FileI
|
||||
if !isViewable {
|
||||
return nil, errors.ErrNotIndexed
|
||||
}
|
||||
} else if isSkipped {
|
||||
return nil, errors.ErrNotIndexed
|
||||
}
|
||||
} else if isSkipped {
|
||||
return nil, errors.ErrNotIndexed
|
||||
}
|
||||
|
||||
realSize, _ := idx.handleFile(dirInfo, indexPath, realPath, false, nil)
|
||||
fileInfo := &iteminfo.FileInfo{
|
||||
realSize, _ := idx.handleFile(dirInfo, indexPath, realPath, false, nil)
|
||||
fileInfo := &iteminfo.FileInfo{
|
||||
Path: indexPath,
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Name: baseName,
|
||||
@@ -1613,9 +1619,9 @@ func (idx *Index) Save() error {
|
||||
|
||||
idx.mu.RLock()
|
||||
// Collect scanner information
|
||||
scanners := make(map[string]*indexingdb.PersistedScannerInfo)
|
||||
scanners := make(map[string]*dbindex.PersistedScannerInfo)
|
||||
for path, scanner := range idx.scanners {
|
||||
scanners[path] = &indexingdb.PersistedScannerInfo{
|
||||
scanners[path] = &dbindex.PersistedScannerInfo{
|
||||
Path: path,
|
||||
Complexity: scanner.complexity,
|
||||
CurrentSchedule: scanner.currentSchedule,
|
||||
@@ -1634,7 +1640,7 @@ func (idx *Index) Save() error {
|
||||
idx.mu.RUnlock()
|
||||
|
||||
// Create IndexInfo for persistence
|
||||
info := &indexingdb.IndexInfo{
|
||||
info := &dbindex.IndexInfo{
|
||||
Path: idx.Path, // Use real filesystem path as key
|
||||
Source: idx.Name,
|
||||
Complexity: complexity,
|
||||
|
||||
@@ -16,7 +16,7 @@ func setupTestIndex(t *testing.T) (*Index, string, func()) {
|
||||
// Initialize the database if not already done
|
||||
if indexDB == nil {
|
||||
var err error
|
||||
indexDB, err = dbsql.NewIndexDB("test_indexing", "OFF", 1000, 32, false)
|
||||
indexDB, _, err = dbsql.NewIndexDB("test_indexing", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
indexingdb "github.com/gtsteffaniak/filebrowser/backend/database/indexing"
|
||||
indexingdb "github.com/gtsteffaniak/filebrowser/backend/database/dbindex"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/events"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestMultiScannerMutex(t *testing.T) {
|
||||
// Initialize the database if not already done
|
||||
if indexDB == nil {
|
||||
var err error
|
||||
indexDB, err = dbsql.NewIndexDB("test_multiscanner", "OFF", 1000, 32, false)
|
||||
indexDB, _, err = dbsql.NewIndexDB("test_multiscanner", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ var ResizableImageTypes = map[string]bool{
|
||||
".tif": true,
|
||||
".heic": true,
|
||||
".heif": true,
|
||||
".webp": true,
|
||||
".pbm": true,
|
||||
".pgm": true,
|
||||
".ppm": true,
|
||||
".pam": true,
|
||||
".other": false,
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ func setupMutateTestIndex(t *testing.T) *Index {
|
||||
}
|
||||
|
||||
var err error
|
||||
testDB, err := dbsql.NewIndexDB("test_init", "OFF", 1000, 32, false)
|
||||
testDB, _, err := dbsql.NewIndexDB("test_init", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
@@ -214,7 +214,7 @@ func TestUpdateFileMetadata(t *testing.T) {
|
||||
// Initialize the database if not already done
|
||||
if indexDB == nil {
|
||||
var err error
|
||||
indexDB, err = dbsql.NewIndexDB("test_mutate", "OFF", 1000, 32, false)
|
||||
indexDB, _, err = dbsql.NewIndexDB("test_mutate", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
@@ -274,7 +274,7 @@ func TestSetDirectoryInfo(t *testing.T) {
|
||||
// Initialize the database if not already done
|
||||
if indexDB == nil {
|
||||
var err error
|
||||
indexDB, err = dbsql.NewIndexDB("test_mutate", "OFF", 1000, 32, false)
|
||||
indexDB, _, err = dbsql.NewIndexDB("test_mutate", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestSearchWhileIndexing(t *testing.T) {
|
||||
// Initialize the database if not already done
|
||||
if indexDB == nil {
|
||||
var err error
|
||||
indexDB, err = dbsql.NewIndexDB("test_search_while", "OFF", 1000, 32, false)
|
||||
indexDB, _, err = dbsql.NewIndexDB("test_search_while", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func TestSearchIndexes(t *testing.T) {
|
||||
// Initialize the database if not already done
|
||||
if indexDB == nil {
|
||||
var err error
|
||||
indexDB, err = dbsql.NewIndexDB("test_search", "OFF", 1000, 32, false)
|
||||
indexDB, _, err = dbsql.NewIndexDB("test_search", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
@@ -462,7 +462,7 @@ func TestSearchLargestModeExcludesRoot(t *testing.T) {
|
||||
// Initialize the database if not already done
|
||||
if indexDB == nil {
|
||||
var err error
|
||||
indexDB, err = dbsql.NewIndexDB("test_search_largest", "OFF", 1000, 32, false)
|
||||
indexDB, _, err = dbsql.NewIndexDB("test_search_largest", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
@@ -558,7 +558,7 @@ func TestSearchLargestModeExcludesScopeDirectory(t *testing.T) {
|
||||
// Initialize the database if not already done
|
||||
if indexDB == nil {
|
||||
var err error
|
||||
indexDB, err = dbsql.NewIndexDB("test_search_scope", "OFF", 1000, 32, false)
|
||||
indexDB, _, err = dbsql.NewIndexDB("test_search_scope", "OFF", 1000, 32, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ userDefaults:
|
||||
deleteWithoutConfirming: false # delete files without confirmation
|
||||
disableOnlyOfficeExt: ".md .txt .pdf" # list of file extensions to disable onlyoffice editor for
|
||||
showSelectMultiple: false # show select multiple files on desktop
|
||||
defaultLandingPage: "" # default landing page to use if no redirect is specified: eg. /files/mysource/mysubpath, /settings, etc.
|
||||
|
||||
#office:
|
||||
# url: "http://localhost:9052"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package preview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// bufferPool manages reusable buffers to reduce allocations
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
// Create 128KB buffers by default
|
||||
return bytes.NewBuffer(make([]byte, 0, 128*1024))
|
||||
},
|
||||
}
|
||||
|
||||
// getBuffer gets a buffer from the pool
|
||||
func getBuffer() *bytes.Buffer {
|
||||
buf, ok := bufferPool.Get().(*bytes.Buffer)
|
||||
if !ok {
|
||||
// This should never happen, but provide a fallback
|
||||
buf = bytes.NewBuffer(make([]byte, 0, 128*1024))
|
||||
}
|
||||
buf.Reset()
|
||||
return buf
|
||||
}
|
||||
|
||||
// putBuffer returns a buffer to the pool
|
||||
func putBuffer(buf *bytes.Buffer) {
|
||||
// Don't pool buffers that grew too large (>1MB)
|
||||
if buf.Cap() > 1024*1024 {
|
||||
return
|
||||
}
|
||||
buf.Reset()
|
||||
bufferPool.Put(buf)
|
||||
}
|
||||
@@ -8,16 +8,16 @@ import (
|
||||
// convertHEICToJPEGWithFFmpeg converts a HEIC file to JPEG format using FFmpeg
|
||||
// This function handles all FFmpeg-related logic and parameters
|
||||
func (s *Service) convertHEICToJPEGWithFFmpeg(ctx context.Context, filePath string, previewSize string) ([]byte, error) {
|
||||
// Check if FFmpeg service is available
|
||||
if s.imageService == nil {
|
||||
return nil, fmt.Errorf("FFmpeg is not available")
|
||||
}
|
||||
|
||||
// Check if context is cancelled before starting
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// Use the shared image service
|
||||
if s.imageService == nil {
|
||||
return nil, fmt.Errorf("image service not available")
|
||||
}
|
||||
|
||||
// Acquire image service semaphore
|
||||
if err := s.imageService.Acquire(ctx); err != nil {
|
||||
return nil, err
|
||||
@@ -39,10 +39,46 @@ func (s *Service) convertHEICToJPEGWithFFmpeg(ctx context.Context, filePath stri
|
||||
width, height = 256, 256
|
||||
quality = "5" // Medium quality
|
||||
}
|
||||
|
||||
// Use tile-based conversion for correct full-resolution image reconstruction
|
||||
result, err := s.imageService.ConvertHEICToJPEG(ctx, filePath, width, height, quality)
|
||||
if err != nil {
|
||||
return s.imageService.ConvertHEICToJPEG(ctx, filePath, width, height, quality)
|
||||
}
|
||||
|
||||
// convertImageWithFFmpeg converts any image file (including problematic JPEGs) to resized JPEG using FFmpeg
|
||||
// This is used as a fallback for JPEG files that Go's standard decoder can't handle (extended sequential, etc.)
|
||||
func (s *Service) convertImageWithFFmpeg(ctx context.Context, filePath string, previewSize string) ([]byte, error) {
|
||||
// Check if FFmpeg service is available
|
||||
if s.imageService == nil {
|
||||
return nil, fmt.Errorf("FFmpeg is not available for JPEG fallback")
|
||||
}
|
||||
|
||||
// Check if context is cancelled before starting
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// Acquire image service semaphore
|
||||
if err := s.imageService.Acquire(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
defer s.imageService.Release()
|
||||
|
||||
// Determine target dimensions and quality based on preview size
|
||||
var width, height int
|
||||
var quality string
|
||||
switch previewSize {
|
||||
case "large":
|
||||
width, height = 640, 640
|
||||
quality = "2" // High quality for FFmpeg -q:v
|
||||
case "original":
|
||||
// For original size - no scaling, maximum quality
|
||||
width, height = 0, 0 // Signal to not apply scaling
|
||||
quality = "1" // Maximum quality for original
|
||||
default:
|
||||
width, height = 256, 256
|
||||
quality = "5" // Medium quality
|
||||
}
|
||||
|
||||
// Use direct conversion which works for all image formats including problematic JPEGs
|
||||
return s.imageService.ConvertImageToJPEG(ctx, filePath, width, height, quality)
|
||||
}
|
||||
+354
-55
@@ -5,7 +5,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
exif "github.com/dsoprea/go-exif/v3"
|
||||
exifcommon "github.com/dsoprea/go-exif/v3/common"
|
||||
@@ -21,6 +23,11 @@ gif
|
||||
tiff
|
||||
bmp
|
||||
heic
|
||||
webp
|
||||
pbm
|
||||
pgm
|
||||
ppm
|
||||
pam
|
||||
)
|
||||
*/
|
||||
type Format int
|
||||
@@ -39,6 +46,16 @@ func (x Format) toImaging() imaging.Format {
|
||||
return imaging.BMP
|
||||
case FormatHeic:
|
||||
return imaging.JPEG
|
||||
case FormatWebp:
|
||||
return imaging.WEBP
|
||||
case FormatPbm:
|
||||
return imaging.PBM
|
||||
case FormatPgm:
|
||||
return imaging.PGM
|
||||
case FormatPpm:
|
||||
return imaging.PPM
|
||||
case FormatPam:
|
||||
return imaging.PAM
|
||||
default:
|
||||
return imaging.JPEG
|
||||
}
|
||||
@@ -56,7 +73,8 @@ type Quality int
|
||||
func (x Quality) resampleFilter() imaging.ResampleFilter {
|
||||
switch x {
|
||||
case QualityHigh:
|
||||
return imaging.Lanczos
|
||||
// Use CatmullRom instead of Lanczos - faster with similar quality for thumbnails
|
||||
return imaging.CatmullRom
|
||||
case QualityMedium:
|
||||
return imaging.Box
|
||||
case QualityLow:
|
||||
@@ -66,6 +84,20 @@ func (x Quality) resampleFilter() imaging.ResampleFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// jpegQuality returns JPEG quality (1-100) based on Quality setting
|
||||
func (x Quality) jpegQuality() int {
|
||||
switch x {
|
||||
case QualityHigh:
|
||||
return 85 // Good quality, faster than 95+
|
||||
case QualityMedium:
|
||||
return 75 // Balanced quality/speed
|
||||
case QualityLow:
|
||||
return 65 // Lower quality, faster encoding
|
||||
default:
|
||||
return 75
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ENUM(
|
||||
fit
|
||||
@@ -96,15 +128,26 @@ func (s *Service) FormatFromExtension(ext string) (Format, error) {
|
||||
return FormatTiff, nil
|
||||
case imaging.BMP:
|
||||
return FormatBmp, nil
|
||||
case imaging.WEBP:
|
||||
return FormatWebp, nil
|
||||
case imaging.PBM:
|
||||
return FormatPbm, nil
|
||||
case imaging.PGM:
|
||||
return FormatPgm, nil
|
||||
case imaging.PPM:
|
||||
return FormatPpm, nil
|
||||
case imaging.PAM:
|
||||
return FormatPam, nil
|
||||
default:
|
||||
return -1, ErrUnsupportedFormat
|
||||
}
|
||||
}
|
||||
|
||||
type resizeConfig struct {
|
||||
format Format
|
||||
resizeMode ResizeMode
|
||||
quality Quality
|
||||
format Format
|
||||
resizeMode ResizeMode
|
||||
quality Quality
|
||||
jpegQuality int // JPEG encoding quality (1-100)
|
||||
}
|
||||
|
||||
type Option func(*resizeConfig)
|
||||
@@ -124,119 +167,375 @@ func WithMode(mode ResizeMode) Option {
|
||||
func WithQuality(quality Quality) Option {
|
||||
return func(config *resizeConfig) {
|
||||
config.quality = quality
|
||||
config.jpegQuality = quality.jpegQuality()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Resize(in io.Reader, width, height int, out io.Writer, options ...Option) error {
|
||||
// Use image service semaphore for image processing
|
||||
if s.imageService != nil {
|
||||
if err := s.imageService.Acquire(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.imageService.Release()
|
||||
}
|
||||
|
||||
format, wrappedReader, err := s.detectFormat(in)
|
||||
// First, detect format and get dimensions (lightweight operation)
|
||||
format, wrappedReader, imgWidth, imgHeight, err := s.detectFormatAndSize(in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect image format: %w", err)
|
||||
}
|
||||
|
||||
// Use image service semaphore for image processing
|
||||
if s.imageService != nil {
|
||||
if acquireErr := s.imageService.Acquire(context.Background()); acquireErr != nil {
|
||||
return acquireErr
|
||||
}
|
||||
defer s.imageService.Release()
|
||||
}
|
||||
|
||||
config := resizeConfig{
|
||||
format: format,
|
||||
resizeMode: ResizeModeFit,
|
||||
quality: QualityMedium,
|
||||
format: format,
|
||||
resizeMode: ResizeModeFit,
|
||||
quality: QualityMedium,
|
||||
jpegQuality: QualityMedium.jpegQuality(),
|
||||
}
|
||||
for _, option := range options {
|
||||
option(&config)
|
||||
}
|
||||
// Ensure JPEG quality is set based on quality setting
|
||||
if config.jpegQuality == 0 {
|
||||
config.jpegQuality = config.quality.jpegQuality()
|
||||
}
|
||||
|
||||
if format == FormatHeic {
|
||||
config.format = FormatJpeg
|
||||
}
|
||||
|
||||
if config.quality == QualityLow && format == FormatJpeg {
|
||||
// Try to use embedded EXIF thumbnail for JPEGs (saves MASSIVE memory and CPU)
|
||||
// This is especially beneficial for large images (e.g., 4000x3000 photos)
|
||||
if format == FormatJpeg && imgWidth > width*2 && imgHeight > height*2 {
|
||||
thm, newWrappedReader, errThm := getEmbeddedThumbnail(wrappedReader)
|
||||
wrappedReader = newWrappedReader
|
||||
if errThm == nil {
|
||||
_, err = out.Write(thm)
|
||||
if err == nil {
|
||||
return nil
|
||||
if errThm == nil && len(thm) > 0 {
|
||||
// Decode the EXIF thumbnail to check its size
|
||||
thmImg, _, thmErr := image.Decode(bytes.NewReader(thm))
|
||||
if thmErr == nil {
|
||||
bounds := thmImg.Bounds()
|
||||
thmWidth := bounds.Dx()
|
||||
thmHeight := bounds.Dy()
|
||||
|
||||
// If EXIF thumbnail is larger than or equal to requested size
|
||||
// and quality is Low, return it as-is (no need to resize down)
|
||||
if config.quality == QualityLow && thmWidth >= width && thmHeight >= height {
|
||||
if config.format == FormatJpeg {
|
||||
return jpeg.Encode(out, thmImg, &jpeg.Options{Quality: config.jpegQuality})
|
||||
}
|
||||
return imaging.Encode(out, thmImg, config.format.toImaging())
|
||||
}
|
||||
|
||||
// If EXIF thumbnail is smaller but still reasonable (at least 40% of target),
|
||||
minThumbSize := width * 40 / 100
|
||||
if thmWidth >= minThumbSize || thmHeight >= minThumbSize {
|
||||
var resizedImg image.Image
|
||||
switch config.resizeMode {
|
||||
case ResizeModeFill:
|
||||
resizedImg = imaging.Fill(thmImg, width, height, imaging.Center, config.quality.resampleFilter())
|
||||
case ResizeModeFit:
|
||||
resizedImg = imaging.Fit(thmImg, width, height, config.quality.resampleFilter())
|
||||
default:
|
||||
resizedImg = imaging.Fit(thmImg, width, height, config.quality.resampleFilter())
|
||||
}
|
||||
|
||||
if config.format == FormatJpeg {
|
||||
return jpeg.Encode(out, resizedImg, &jpeg.Options{Quality: config.jpegQuality})
|
||||
}
|
||||
return imaging.Encode(out, resizedImg, config.format.toImaging())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For HEIC files, try without AutoOrientation first since it might not work properly
|
||||
// Try decoding with default settings (auto color space conversion)
|
||||
img, err := imaging.Decode(wrappedReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode image: %w", err)
|
||||
// If decoding fails due to ICC profile issues (e.g., unsupported grayscale colorspace),
|
||||
// retry with color space conversion disabled
|
||||
if bytes.Contains([]byte(err.Error()), []byte("colorspace")) ||
|
||||
bytes.Contains([]byte(err.Error()), []byte("ICC profile")) {
|
||||
// Reset reader to beginning
|
||||
if seeker, ok := wrappedReader.(io.Seeker); ok {
|
||||
_, _ = seeker.Seek(0, io.SeekStart)
|
||||
}
|
||||
// Try again without color space conversion
|
||||
img, err = imaging.Decode(wrappedReader, imaging.ColorSpace(imaging.NO_CHANGE_OF_COLORSPACE))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: For HEIC files processed via FFmpeg, orientation is handled automatically
|
||||
|
||||
// Use the quality setting requested by caller
|
||||
resampleFilter := config.quality.resampleFilter()
|
||||
|
||||
switch config.resizeMode {
|
||||
case ResizeModeFill:
|
||||
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
|
||||
img = imaging.Fill(img, width, height, imaging.Center, resampleFilter)
|
||||
case ResizeModeFit:
|
||||
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
|
||||
img = imaging.Fit(img, width, height, resampleFilter)
|
||||
default:
|
||||
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
|
||||
img = imaging.Fit(img, width, height, resampleFilter)
|
||||
}
|
||||
|
||||
// Use optimized JPEG encoding with quality control for better performance
|
||||
if config.format == FormatJpeg {
|
||||
// Further optimize quality for small thumbnails
|
||||
jpegQuality := config.jpegQuality
|
||||
return jpeg.Encode(out, img, &jpeg.Options{Quality: jpegQuality})
|
||||
}
|
||||
|
||||
return imaging.Encode(out, img, config.format.toImaging())
|
||||
}
|
||||
|
||||
func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) {
|
||||
// Read all data into a buffer first to avoid consuming the reader twice
|
||||
// detectFormatAndSize detects format and dimensions without full decode
|
||||
func (s *Service) detectFormatAndSize(in io.Reader) (Format, io.Reader, int, int, error) {
|
||||
const maxHeaderSize = 64 * 1024 // 64KB should be enough for format detection
|
||||
|
||||
// Try to work with seekable readers efficiently
|
||||
if seeker, ok := in.(io.Seeker); ok {
|
||||
originalPos, err := seeker.Seek(0, io.SeekCurrent)
|
||||
if err == nil {
|
||||
// Seek to start
|
||||
if _, err := seeker.Seek(0, io.SeekStart); err == nil {
|
||||
headerBuf := make([]byte, maxHeaderSize)
|
||||
n, readErr := in.Read(headerBuf)
|
||||
// Handle EOF gracefully (file smaller than header size)
|
||||
if readErr == nil || readErr == io.EOF {
|
||||
if n >= 2 {
|
||||
headerBuf = headerBuf[:n]
|
||||
isFullFile := (readErr == io.EOF) || (n < maxHeaderSize)
|
||||
|
||||
// Try magic byte detection first (fastest)
|
||||
format := s.detectFormatFromMagicBytes(headerBuf, n)
|
||||
|
||||
// Try to decode config for dimensions
|
||||
reader := bytes.NewReader(headerBuf)
|
||||
config, imgFormat, configErr := image.DecodeConfig(reader)
|
||||
|
||||
if configErr == nil {
|
||||
// Successfully got config
|
||||
if format < 0 {
|
||||
format = s.parseFormat(imgFormat)
|
||||
}
|
||||
if format >= 0 {
|
||||
if isFullFile {
|
||||
// Small file - use the buffer we already have
|
||||
return format, bytes.NewReader(headerBuf), config.Width, config.Height, nil
|
||||
}
|
||||
// Large file - seek back and use original reader
|
||||
_, _ = seeker.Seek(originalPos, io.SeekStart)
|
||||
return format, in, config.Width, config.Height, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Restore position and fall through
|
||||
_, _ = seeker.Seek(originalPos, io.SeekStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: read all (for non-seekable or if header detection failed)
|
||||
allData, err := io.ReadAll(in)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed to read image data: %w", err)
|
||||
return -1, nil, 0, 0, fmt.Errorf("failed to read image data: %w", err)
|
||||
}
|
||||
if len(allData) == 0 {
|
||||
return -1, nil, 0, 0, fmt.Errorf("image data is empty: %w", ErrUnsupportedFormat)
|
||||
}
|
||||
|
||||
// Create a reader for format detection
|
||||
reader := bytes.NewReader(allData)
|
||||
_, imgFormat, err := image.DecodeConfig(reader)
|
||||
config, imgFormat, err := image.DecodeConfig(reader)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("image.DecodeConfig failed (data size: %d bytes): %s: %w", len(allData), err.Error(), ErrUnsupportedFormat)
|
||||
return -1, nil, 0, 0, fmt.Errorf("image.DecodeConfig failed: %w", ErrUnsupportedFormat)
|
||||
}
|
||||
|
||||
format := s.parseFormat(imgFormat)
|
||||
if format < 0 {
|
||||
return -1, nil, 0, 0, fmt.Errorf("unsupported image format '%s': %w", imgFormat, ErrUnsupportedFormat)
|
||||
}
|
||||
|
||||
return format, bytes.NewReader(allData), config.Width, config.Height, nil
|
||||
}
|
||||
|
||||
// detectFormatFromMagicBytes detects image format from magic bytes in file header
|
||||
// Returns -1 if no format is detected (since FormatJpeg = 0)
|
||||
func (s *Service) detectFormatFromMagicBytes(header []byte, n int) Format {
|
||||
if n < 2 {
|
||||
return -1
|
||||
}
|
||||
|
||||
// JPEG: FF D8 (the third byte can vary, so we only check first two)
|
||||
if header[0] == 0xFF && header[1] == 0xD8 {
|
||||
return FormatJpeg
|
||||
}
|
||||
|
||||
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
||||
if n >= 8 && header[0] == 0x89 && header[1] == 0x50 && header[2] == 0x4E && header[3] == 0x47 {
|
||||
return FormatPng
|
||||
}
|
||||
|
||||
// GIF: GIF87a or GIF89a
|
||||
if n >= 6 && (string(header[0:6]) == "GIF87a" || string(header[0:6]) == "GIF89a") {
|
||||
return FormatGif
|
||||
}
|
||||
|
||||
// BMP: BM (42 4D)
|
||||
if n >= 2 && header[0] == 0x42 && header[1] == 0x4D {
|
||||
return FormatBmp
|
||||
}
|
||||
|
||||
// TIFF: Little-endian (49 49 2A 00) or Big-endian (4D 4D 00 2A)
|
||||
if n >= 4 {
|
||||
if (header[0] == 0x49 && header[1] == 0x49 && header[2] == 0x2A && header[3] == 0x00) ||
|
||||
(header[0] == 0x4D && header[1] == 0x4D && header[2] == 0x00 && header[3] == 0x2A) {
|
||||
return FormatTiff
|
||||
}
|
||||
}
|
||||
|
||||
// WebP: RIFF...WEBP (starts with RIFF, contains WEBP at offset 8)
|
||||
if n >= 12 && string(header[0:4]) == "RIFF" && string(header[8:12]) == "WEBP" {
|
||||
return FormatWebp
|
||||
}
|
||||
|
||||
// HEIC/HEIF: ftyp box (starts with 4-byte size, then 'ftyp', then brand)
|
||||
// Common brands: heic, heif, mif1
|
||||
if n >= 12 {
|
||||
// Check for ftyp at offset 4
|
||||
if string(header[4:8]) == "ftyp" {
|
||||
// Check for HEIC/HEIF brands
|
||||
brand := string(header[8:12])
|
||||
if brand == "heic" || brand == "heif" || brand == "mif1" {
|
||||
return FormatHeic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Netpbm formats: P1-P7 (ASCII) or binary variants
|
||||
if n >= 2 && header[0] == 'P' {
|
||||
switch header[1] {
|
||||
case '1', '4': // PBM (P1=ASCII, P4=binary)
|
||||
return FormatPbm
|
||||
case '2', '5': // PGM (P2=ASCII, P5=binary)
|
||||
return FormatPgm
|
||||
case '3', '6': // PPM (P3=ASCII, P6=binary)
|
||||
return FormatPpm
|
||||
case '7': // PAM
|
||||
return FormatPam
|
||||
}
|
||||
}
|
||||
|
||||
return -1 // No format detected
|
||||
}
|
||||
|
||||
// parseFormat parses the image format string and returns the Format enum
|
||||
func (s *Service) parseFormat(imgFormat string) Format {
|
||||
if imgFormat == "heif" {
|
||||
imgFormat = "heic"
|
||||
}
|
||||
// Handle case variations - image.DecodeConfig might return "jpeg" or "JPEG"
|
||||
imgFormat = strings.ToLower(imgFormat)
|
||||
|
||||
format, err := ParseFormat(imgFormat)
|
||||
if err != nil {
|
||||
return 0, nil, ErrUnsupportedFormat
|
||||
// Manual mapping to ensure compatibility (bypasses generated enum ParseFormat)
|
||||
switch imgFormat {
|
||||
case "jpeg", "jpg":
|
||||
return FormatJpeg
|
||||
case "png":
|
||||
return FormatPng
|
||||
case "gif":
|
||||
return FormatGif
|
||||
case "tiff", "tif":
|
||||
return FormatTiff
|
||||
case "bmp":
|
||||
return FormatBmp
|
||||
case "heic", "heif":
|
||||
return FormatHeic
|
||||
case "webp":
|
||||
return FormatWebp
|
||||
case "pbm":
|
||||
return FormatPbm
|
||||
case "pgm":
|
||||
return FormatPgm
|
||||
case "ppm":
|
||||
return FormatPpm
|
||||
case "pam":
|
||||
return FormatPam
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
|
||||
// Return a new reader with all the data for subsequent operations
|
||||
return format, bytes.NewReader(allData), nil
|
||||
}
|
||||
|
||||
func getEmbeddedThumbnail(in io.Reader) ([]byte, io.Reader, error) {
|
||||
// Read all data to avoid partial consumption issues
|
||||
allData, err := io.ReadAll(in)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
// Optimize memory: only read enough bytes for EXIF parsing (max 64KB)
|
||||
// EXIF data is typically in the first 64KB of JPEG files
|
||||
const maxExifSize = 0xffff // 64KB
|
||||
|
||||
var headerBuf []byte
|
||||
var wrappedReader io.Reader
|
||||
|
||||
// Check if reader is seekable - if so, we can peek and seek back (most efficient)
|
||||
if seeker, ok := in.(io.Seeker); ok {
|
||||
originalPos, err := seeker.Seek(0, io.SeekCurrent)
|
||||
if err == nil {
|
||||
// Use buffer pool for temporary storage
|
||||
buf := getBuffer()
|
||||
defer putBuffer(buf)
|
||||
|
||||
// Read just the header
|
||||
limitedReader := io.LimitReader(in, maxExifSize)
|
||||
_, readErr := buf.ReadFrom(limitedReader)
|
||||
if readErr == nil || readErr == io.EOF {
|
||||
// Copy to permanent storage for EXIF parsing
|
||||
headerBuf = make([]byte, buf.Len())
|
||||
copy(headerBuf, buf.Bytes())
|
||||
// Seek back to start for main processing
|
||||
_, _ = seeker.Seek(originalPos, io.SeekStart)
|
||||
wrappedReader = in
|
||||
} else {
|
||||
// Read error - fallback to reading all
|
||||
_, _ = seeker.Seek(originalPos, io.SeekStart)
|
||||
allData, _ := io.ReadAll(in)
|
||||
headerBuf = allData
|
||||
wrappedReader = bytes.NewReader(allData)
|
||||
}
|
||||
} else {
|
||||
// Seek failed, read all
|
||||
allData, _ := io.ReadAll(in)
|
||||
headerBuf = allData
|
||||
wrappedReader = bytes.NewReader(allData)
|
||||
}
|
||||
} else {
|
||||
// Non-seekable reader - must read and combine
|
||||
buf := make([]byte, maxExifSize)
|
||||
n, err := io.ReadFull(in, buf)
|
||||
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
|
||||
return nil, nil, err
|
||||
}
|
||||
headerBuf = buf[:n]
|
||||
// Read remaining data and combine
|
||||
remaining, _ := io.ReadAll(in)
|
||||
combined := append(headerBuf, remaining...)
|
||||
wrappedReader = bytes.NewReader(combined)
|
||||
}
|
||||
|
||||
// Create a reader that can be returned for fallback processing
|
||||
wrappedReader := bytes.NewReader(allData)
|
||||
|
||||
// Try to read up to 0xffff bytes for EXIF header parsing
|
||||
// Use the actual data size if file is smaller
|
||||
headSize := 0xffff
|
||||
if len(allData) < headSize {
|
||||
headSize = len(allData)
|
||||
}
|
||||
|
||||
head := allData[:headSize]
|
||||
// Try to find EXIF header
|
||||
offsets := []int{12, 30}
|
||||
|
||||
var offset int
|
||||
var exifErr error
|
||||
for _, offset = range offsets {
|
||||
if _, exifErr = exif.ParseExifHeader(head[offset:]); exifErr == nil {
|
||||
break
|
||||
if offset < len(headerBuf) {
|
||||
if _, exifErr = exif.ParseExifHeader(headerBuf[offset:]); exifErr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +548,7 @@ func getEmbeddedThumbnail(in io.Reader) ([]byte, io.Reader, error) {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
_, index, err := exif.Collect(im, exif.NewTagIndex(), head[offset:])
|
||||
_, index, err := exif.Collect(im, exif.NewTagIndex(), headerBuf[offset:])
|
||||
if err != nil {
|
||||
return nil, wrappedReader, err
|
||||
}
|
||||
|
||||
@@ -291,7 +291,6 @@ func TestService_Resize(t *testing.T) {
|
||||
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Use a temporary directory for cache to avoid creating directories in the source tree
|
||||
tmpDir := t.TempDir()
|
||||
svc := NewPreviewGenerator(1, tmpDir)
|
||||
source := test.source(t)
|
||||
@@ -439,10 +438,26 @@ func TestService_FormatFromExtension(t *testing.T) {
|
||||
ext: ".tiff",
|
||||
want: FormatTiff,
|
||||
},
|
||||
"tif": {
|
||||
ext: ".tif",
|
||||
want: FormatTiff,
|
||||
},
|
||||
"bmp": {
|
||||
ext: ".bmp",
|
||||
want: FormatBmp,
|
||||
},
|
||||
"heic": {
|
||||
ext: ".heic",
|
||||
want: FormatHeic,
|
||||
},
|
||||
"heif": {
|
||||
ext: ".heif",
|
||||
want: FormatHeic,
|
||||
},
|
||||
"webp": {
|
||||
ext: ".webp",
|
||||
want: FormatWebp,
|
||||
},
|
||||
"unknown": {
|
||||
ext: ".mov",
|
||||
wantErr: ErrUnsupportedFormat,
|
||||
@@ -451,7 +466,6 @@ func TestService_FormatFromExtension(t *testing.T) {
|
||||
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Use a temporary directory for cache to avoid creating directories in the source tree
|
||||
tmpDir := t.TempDir()
|
||||
svc := NewPreviewGenerator(1, tmpDir)
|
||||
got, err := svc.FormatFromExtension(test.ext)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package preview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// MemoryTracker wraps the existing FFmpegService to add memory tracking
|
||||
type MemoryTracker struct {
|
||||
maxMemoryBytes int64
|
||||
usedMemoryBytes int64
|
||||
semaphore chan struct{}
|
||||
}
|
||||
|
||||
// NewMemoryTracker creates a memory-aware concurrency limiter
|
||||
func NewMemoryTracker(maxConcurrent int, maxMemoryMB int) *MemoryTracker {
|
||||
if maxConcurrent < 1 {
|
||||
maxConcurrent = 1
|
||||
}
|
||||
if maxMemoryMB < 50 {
|
||||
maxMemoryMB = 500 // Default 500MB
|
||||
}
|
||||
return &MemoryTracker{
|
||||
maxMemoryBytes: int64(maxMemoryMB) * 1024 * 1024,
|
||||
semaphore: make(chan struct{}, maxConcurrent),
|
||||
}
|
||||
}
|
||||
|
||||
// TryAcquire attempts to acquire resources for processing an image
|
||||
// Returns false if memory limit would be exceeded
|
||||
func (mt *MemoryTracker) TryAcquire(ctx context.Context, estimatedBytes int64) bool {
|
||||
// Check if we have memory headroom
|
||||
currentUsed := atomic.LoadInt64(&mt.usedMemoryBytes)
|
||||
if currentUsed+estimatedBytes > mt.maxMemoryBytes {
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to acquire semaphore slot (non-blocking check first)
|
||||
select {
|
||||
case mt.semaphore <- struct{}{}:
|
||||
// Successfully acquired slot, now add memory
|
||||
atomic.AddInt64(&mt.usedMemoryBytes, estimatedBytes)
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire waits to acquire resources
|
||||
func (mt *MemoryTracker) Acquire(ctx context.Context, estimatedBytes int64) error {
|
||||
// Wait for semaphore slot
|
||||
select {
|
||||
case mt.semaphore <- struct{}{}:
|
||||
atomic.AddInt64(&mt.usedMemoryBytes, estimatedBytes)
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Release frees up resources
|
||||
func (mt *MemoryTracker) Release(estimatedBytes int64) {
|
||||
<-mt.semaphore
|
||||
atomic.AddInt64(&mt.usedMemoryBytes, -estimatedBytes)
|
||||
}
|
||||
|
||||
// GetMemoryUsageMB returns current memory usage in MB
|
||||
func (mt *MemoryTracker) GetMemoryUsageMB() int64 {
|
||||
return atomic.LoadInt64(&mt.usedMemoryBytes) / (1024 * 1024)
|
||||
}
|
||||
+64
-19
@@ -29,14 +29,15 @@ var (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
fileCache diskcache.Interface
|
||||
cacheDir string // Cache directory used for thumbnails and temp files
|
||||
debug bool
|
||||
docGenMutex sync.Mutex // Mutex to serialize access to doc generation
|
||||
docSemaphore chan struct{} // Semaphore for document generation
|
||||
officeSem chan struct{} // Semaphore for office document processing
|
||||
videoService *ffmpeg.FFmpegService
|
||||
imageService *ffmpeg.FFmpegService
|
||||
fileCache diskcache.Interface
|
||||
cacheDir string // Cache directory used for thumbnails and temp files
|
||||
debug bool
|
||||
docGenMutex sync.Mutex // Mutex to serialize access to doc generation
|
||||
docSemaphore chan struct{} // Semaphore for document generation
|
||||
officeSem chan struct{} // Semaphore for office document processing
|
||||
videoService *ffmpeg.FFmpegService
|
||||
imageService *ffmpeg.FFmpegService
|
||||
memoryTracker *MemoryTracker // Memory-aware tracker for image processing
|
||||
}
|
||||
|
||||
func NewPreviewGenerator(concurrencyLimit int, cacheDir string) *Service {
|
||||
@@ -76,17 +77,22 @@ func NewPreviewGenerator(concurrencyLimit int, cacheDir string) *Service {
|
||||
videoService := ffmpeg.NewFFmpegService(ffmpegConcurrencyLimit, settings.Config.Integrations.Media.Debug, "")
|
||||
imageService := ffmpeg.NewFFmpegService(concurrencyLimit, settings.Config.Integrations.Media.Debug, filepath.Join(actualCacheDir, "heic"))
|
||||
|
||||
// Create memory tracker for image processing
|
||||
// Limit to 500MB of concurrent image processing memory
|
||||
maxMemoryMB := 500
|
||||
memoryTracker := NewMemoryTracker(concurrencyLimit, maxMemoryMB)
|
||||
|
||||
settings.Env.MuPdfAvailable = docEnabled()
|
||||
logger.Debugf("MuPDF Enabled : %v", settings.Env.MuPdfAvailable)
|
||||
logger.Debugf("Media Enabled : %v", settings.MediaEnabled())
|
||||
|
||||
return &Service{
|
||||
fileCache: fileCache,
|
||||
cacheDir: actualCacheDir,
|
||||
debug: settings.Config.Integrations.Media.Debug,
|
||||
docSemaphore: make(chan struct{}, 1), // must be 1 because cgo thread limit
|
||||
officeSem: make(chan struct{}, concurrencyLimit),
|
||||
videoService: videoService,
|
||||
imageService: imageService,
|
||||
fileCache: fileCache,
|
||||
cacheDir: actualCacheDir,
|
||||
debug: settings.Config.Integrations.Media.Debug,
|
||||
docSemaphore: make(chan struct{}, 1), // must be 1 because cgo thread limit
|
||||
officeSem: make(chan struct{}, concurrencyLimit),
|
||||
videoService: videoService,
|
||||
imageService: imageService,
|
||||
memoryTracker: memoryTracker,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +190,10 @@ func GeneratePreviewWithMD5(ctx context.Context, file iteminfo.ExtendedFileInfo,
|
||||
hasher := md5.New()
|
||||
_, _ = hasher.Write([]byte(CacheKey(fileMD5, previewSize, seekPercentage)))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
convertHEIC := strings.HasPrefix(file.Type, "image/heic") && settings.Config.Integrations.Media.Convert.ImagePreview[settings.HEICImagePreview]
|
||||
|
||||
// Check if HEIC conversion is enabled (guaranteed non-nil after defaults)
|
||||
convertHEIC := strings.HasPrefix(file.Type, "image/heic") && *settings.Config.Integrations.Media.Convert.ImagePreview[settings.HEICImagePreview]
|
||||
|
||||
// Generate an image from office document
|
||||
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
|
||||
tempFilePath := filepath.Join(service.cacheDir, "thumbnails", "docs", hash) + ".txt"
|
||||
@@ -216,6 +225,12 @@ func GeneratePreviewWithMD5(ctx context.Context, file iteminfo.ExtendedFileInfo,
|
||||
return nil, fmt.Errorf("failed to read image file: %w", err)
|
||||
}
|
||||
} else if strings.HasPrefix(file.Type, "video") {
|
||||
// Check if this video format is enabled for preview generation
|
||||
ext = strings.TrimPrefix(strings.ToLower(filepath.Ext(file.Name)), ".")
|
||||
if !settings.CanConvertVideo(ext) {
|
||||
return nil, fmt.Errorf("video preview generation is disabled for .%s files in settings", ext)
|
||||
}
|
||||
|
||||
videoSeekPercentage := seekPercentage
|
||||
if videoSeekPercentage == 0 {
|
||||
videoSeekPercentage = 10
|
||||
@@ -258,6 +273,37 @@ func GeneratePreviewWithMD5(ctx context.Context, file iteminfo.ExtendedFileInfo,
|
||||
// resize image
|
||||
resizedBytes, err := service.CreatePreview(imageBytes, previewSize)
|
||||
if err != nil {
|
||||
// Check if this is an unsupported JPEG format (extended sequential, etc.)
|
||||
errMsg := err.Error()
|
||||
if strings.HasPrefix(file.Type, "image/jpeg") &&
|
||||
(strings.Contains(errMsg, "unrecognised marker") ||
|
||||
strings.Contains(errMsg, "invalid JPEG format") ||
|
||||
strings.Contains(errMsg, "Huffman")) {
|
||||
|
||||
// Check if JPEG FFmpeg conversion is enabled (guaranteed non-nil after defaults)
|
||||
enableJPEGFallback := *settings.Config.Integrations.Media.Convert.ImagePreview[settings.JPEGImagePreview]
|
||||
|
||||
// Only attempt FFmpeg fallback if it's enabled and FFmpeg service is available
|
||||
if enableJPEGFallback && service.imageService != nil {
|
||||
// Fall back to FFmpeg for problematic JPEG files
|
||||
logger.Debugf("JPEG decode failed for '%s', falling back to FFmpeg: %v", file.Name, err)
|
||||
resizedBytes, err = service.convertImageWithFFmpeg(ctx, file.RealPath, previewSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resize preview image with FFmpeg fallback: %w", err)
|
||||
}
|
||||
// Cache and return the FFmpeg-converted image
|
||||
cacheKey := CacheKey(fileMD5, previewSize, seekPercentage)
|
||||
if err = service.fileCache.Store(ctx, cacheKey, resizedBytes); err != nil {
|
||||
logger.Errorf("failed to cache FFmpeg-converted image: %v", err)
|
||||
}
|
||||
return resizedBytes, nil
|
||||
}
|
||||
// If FFmpeg fallback is disabled or not available, return the original error
|
||||
if !enableJPEGFallback {
|
||||
return nil, fmt.Errorf("failed to resize preview image (unsupported JPEG format, FFmpeg conversion disabled in settings): %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to resize preview image (unsupported JPEG format, FFmpeg not available): %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to resize preview image: %w", err)
|
||||
}
|
||||
// Cache and return
|
||||
@@ -273,7 +319,6 @@ func GeneratePreviewWithMD5(ctx context.Context, file iteminfo.ExtendedFileInfo,
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GeneratePreview(ctx context.Context, file iteminfo.ExtendedFileInfo, previewSize, officeUrl string, seekPercentage int) ([]byte, error) {
|
||||
|
||||
@@ -1278,7 +1278,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/raw": {
|
||||
"get": {
|
||||
"description": "Returns the raw content of a file, multiple files, or a directory. Supports downloading files as archives in various formats.\n\n**Filename Encoding:**\n- The Content-Disposition header will always include both:\n1. ` + "`" + `filename=\"...\"` + "`" + `: An ASCII-safe version of the filename for compatibility.\n2. ` + "`" + `filename*=utf-8\"...` + "`" + `: The full UTF-8 encoded filename (RFC 6266/5987) for modern clients.",
|
||||
"description": "Returns the raw content of a file, multiple files, or a directory. Supports downloading files as archives in various formats.\n\n**Filename Encoding:**\n- The Content-Disposition header will always include both:\n1. ` + "`" + `filename=\"...\"` + "`" + `: An ASCII-safe version of the filename for compatibility.\n2. ` + "`" + `filename*=utf-8\"...` + "`" + `: The full UTF-8 encoded filename (RFC 6266/5987) for modern clients.\n\n**Multiple Files:**\n- Use repeated query parameters: ` + "`" + `?file=file1.txt\u0026file=file2.txt\u0026file=file3.txt` + "`" + `\n- This supports filenames containing commas and special characters",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -1295,9 +1295,13 @@ const docTemplate = `{
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of file paths (required)",
|
||||
"name": "files",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "File path (can be repeated for multiple files)",
|
||||
"name": "file",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
@@ -2769,7 +2773,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/public/api/raw": {
|
||||
"get": {
|
||||
"description": "Downloads raw content from a public share. Supports single files, multiple files, or directories as archives. Enforces download limits (global or per-user) and blocks anonymous users when per-user limits are enabled.",
|
||||
"description": "Downloads raw content from a public share. Supports single files, multiple files, or directories as archives. Enforces download limits (global or per-user) and blocks anonymous users when per-user limits are enabled.\n\n**Multiple Files:**\n- Use repeated query parameters: ` + "`" + `?file=file1.txt\u0026file=file2.txt\u0026file=file3.txt` + "`" + `\n- This supports filenames containing commas and special characters",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -2789,9 +2793,13 @@ const docTemplate = `{
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of file paths. Example: 'file1.txt,folder/file2.txt'",
|
||||
"name": "files",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "File path (can be repeated for multiple files)",
|
||||
"name": "file",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
@@ -4255,7 +4263,7 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"imagePreview": {
|
||||
"description": "supported image preview formats. defaults to false for all types unless explicitly enabled.",
|
||||
"description": "supported image preview formats. defaults vary by type (see individual type docs)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
@@ -4824,10 +4832,6 @@ const docTemplate = `{
|
||||
"description": "debug onlyoffice editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"defaultLandingPage": {
|
||||
"description": "deprecated: determined by sidebar link order since 1.1.0",
|
||||
"type": "string"
|
||||
},
|
||||
"deleteWithoutConfirming": {
|
||||
"description": "delete files without confirmation",
|
||||
"type": "boolean"
|
||||
@@ -5670,10 +5674,6 @@ const docTemplate = `{
|
||||
"description": "debug onlyoffice editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"defaultLandingPage": {
|
||||
"description": "deprecated: determined by sidebar link order instead",
|
||||
"type": "string"
|
||||
},
|
||||
"deleteWithoutConfirming": {
|
||||
"description": "delete files without confirmation",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -1267,7 +1267,7 @@
|
||||
},
|
||||
"/api/raw": {
|
||||
"get": {
|
||||
"description": "Returns the raw content of a file, multiple files, or a directory. Supports downloading files as archives in various formats.\n\n**Filename Encoding:**\n- The Content-Disposition header will always include both:\n1. `filename=\"...\"`: An ASCII-safe version of the filename for compatibility.\n2. `filename*=utf-8\"...`: The full UTF-8 encoded filename (RFC 6266/5987) for modern clients.",
|
||||
"description": "Returns the raw content of a file, multiple files, or a directory. Supports downloading files as archives in various formats.\n\n**Filename Encoding:**\n- The Content-Disposition header will always include both:\n1. `filename=\"...\"`: An ASCII-safe version of the filename for compatibility.\n2. `filename*=utf-8\"...`: The full UTF-8 encoded filename (RFC 6266/5987) for modern clients.\n\n**Multiple Files:**\n- Use repeated query parameters: `?file=file1.txt\u0026file=file2.txt\u0026file=file3.txt`\n- This supports filenames containing commas and special characters",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -1284,9 +1284,13 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of file paths (required)",
|
||||
"name": "files",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "File path (can be repeated for multiple files)",
|
||||
"name": "file",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
@@ -2758,7 +2762,7 @@
|
||||
},
|
||||
"/public/api/raw": {
|
||||
"get": {
|
||||
"description": "Downloads raw content from a public share. Supports single files, multiple files, or directories as archives. Enforces download limits (global or per-user) and blocks anonymous users when per-user limits are enabled.",
|
||||
"description": "Downloads raw content from a public share. Supports single files, multiple files, or directories as archives. Enforces download limits (global or per-user) and blocks anonymous users when per-user limits are enabled.\n\n**Multiple Files:**\n- Use repeated query parameters: `?file=file1.txt\u0026file=file2.txt\u0026file=file3.txt`\n- This supports filenames containing commas and special characters",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -2778,9 +2782,13 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of file paths. Example: 'file1.txt,folder/file2.txt'",
|
||||
"name": "files",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "File path (can be repeated for multiple files)",
|
||||
"name": "file",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
@@ -4244,7 +4252,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"imagePreview": {
|
||||
"description": "supported image preview formats. defaults to false for all types unless explicitly enabled.",
|
||||
"description": "supported image preview formats. defaults vary by type (see individual type docs)",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
@@ -4813,10 +4821,6 @@
|
||||
"description": "debug onlyoffice editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"defaultLandingPage": {
|
||||
"description": "deprecated: determined by sidebar link order since 1.1.0",
|
||||
"type": "string"
|
||||
},
|
||||
"deleteWithoutConfirming": {
|
||||
"description": "delete files without confirmation",
|
||||
"type": "boolean"
|
||||
@@ -5659,10 +5663,6 @@
|
||||
"description": "debug onlyoffice editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"defaultLandingPage": {
|
||||
"description": "deprecated: determined by sidebar link order instead",
|
||||
"type": "string"
|
||||
},
|
||||
"deleteWithoutConfirming": {
|
||||
"description": "delete files without confirmation",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -606,8 +606,8 @@ definitions:
|
||||
imagePreview:
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: supported image preview formats. defaults to false for all types
|
||||
unless explicitly enabled.
|
||||
description: supported image preview formats. defaults vary by type (see individual
|
||||
type docs)
|
||||
type: object
|
||||
videoPreview:
|
||||
additionalProperties:
|
||||
@@ -1035,9 +1035,6 @@ definitions:
|
||||
debugOffice:
|
||||
description: debug onlyoffice editor
|
||||
type: boolean
|
||||
defaultLandingPage:
|
||||
description: 'deprecated: determined by sidebar link order since 1.1.0'
|
||||
type: string
|
||||
deleteWithoutConfirming:
|
||||
description: delete files without confirmation
|
||||
type: boolean
|
||||
@@ -1641,9 +1638,6 @@ definitions:
|
||||
debugOffice:
|
||||
description: debug onlyoffice editor
|
||||
type: boolean
|
||||
defaultLandingPage:
|
||||
description: 'deprecated: determined by sidebar link order instead'
|
||||
type: string
|
||||
deleteWithoutConfirming:
|
||||
description: delete files without confirmation
|
||||
type: boolean
|
||||
@@ -2624,17 +2618,24 @@ paths:
|
||||
- The Content-Disposition header will always include both:
|
||||
1. `filename="..."`: An ASCII-safe version of the filename for compatibility.
|
||||
2. `filename*=utf-8"...`: The full UTF-8 encoded filename (RFC 6266/5987) for modern clients.
|
||||
|
||||
**Multiple Files:**
|
||||
- Use repeated query parameters: `?file=file1.txt&file=file2.txt&file=file3.txt`
|
||||
- This supports filenames containing commas and special characters
|
||||
parameters:
|
||||
- description: Source name for the files (required)
|
||||
in: query
|
||||
name: source
|
||||
required: true
|
||||
type: string
|
||||
- description: Comma-separated list of file paths (required)
|
||||
- collectionFormat: csv
|
||||
description: File path (can be repeated for multiple files)
|
||||
in: query
|
||||
name: files
|
||||
items:
|
||||
type: string
|
||||
name: file
|
||||
required: true
|
||||
type: string
|
||||
type: array
|
||||
- description: If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults
|
||||
to 'attachment'.
|
||||
in: query
|
||||
@@ -3641,20 +3642,26 @@ paths:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Downloads raw content from a public share. Supports single files,
|
||||
multiple files, or directories as archives. Enforces download limits (global
|
||||
or per-user) and blocks anonymous users when per-user limits are enabled.
|
||||
description: |-
|
||||
Downloads raw content from a public share. Supports single files, multiple files, or directories as archives. Enforces download limits (global or per-user) and blocks anonymous users when per-user limits are enabled.
|
||||
|
||||
**Multiple Files:**
|
||||
- Use repeated query parameters: `?file=file1.txt&file=file2.txt&file=file3.txt`
|
||||
- This supports filenames containing commas and special characters
|
||||
parameters:
|
||||
- description: Share hash for authentication
|
||||
in: query
|
||||
name: hash
|
||||
required: true
|
||||
type: string
|
||||
- description: 'Comma-separated list of file paths. Example: ''file1.txt,folder/file2.txt'''
|
||||
- collectionFormat: csv
|
||||
description: File path (can be repeated for multiple files)
|
||||
in: query
|
||||
name: files
|
||||
items:
|
||||
type: string
|
||||
name: file
|
||||
required: true
|
||||
type: string
|
||||
type: array
|
||||
- description: If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults
|
||||
to 'attachment'.
|
||||
in: query
|
||||
|
||||
@@ -27,6 +27,20 @@ server:
|
||||
private: false # designate as source as private -- currently just means no sharing permitted.
|
||||
disabled: false # disable the source, this is useful so you don't need to remove it from the config file
|
||||
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"
|
||||
fileName: "" # (global) exclude files that match these names. Eg. "file.txt" or "test.csv"
|
||||
folderName: "" # (global) exclude folders that match these names. Eg. "folder" or "subfolder"
|
||||
viewable: false # Enable viewing in UI but exclude from indexing
|
||||
ignoreHidden: false # Excludes only hidden files and folders
|
||||
ignoreZeroSizeFolders: false # Excludes only folders with 0 size
|
||||
ignoreSymlinks: false # Excludes symbolic links
|
||||
defaultUserScope: "/" # defaults to root of index "/" 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 under defaultUserScope + username
|
||||
@@ -165,8 +179,9 @@ integrations:
|
||||
media: # validate:omitempty
|
||||
ffmpegPath: "" # path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)
|
||||
convert: # config for ffmpeg conversion settings
|
||||
imagePreview: # supported image preview formats. defaults to false for all types unless explicitly enabled.
|
||||
imagePreview: # supported image preview formats. defaults vary by type (see individual type docs)
|
||||
heic: false
|
||||
jpeg: true
|
||||
videoPreview: # supported video preview formats. defaults to true for all types unless explicitly disabled.
|
||||
3g2: true
|
||||
3gp: true
|
||||
|
||||
@@ -151,7 +151,7 @@ export async function download(format, files, shareHash = "") {
|
||||
}
|
||||
|
||||
const params = {
|
||||
files: filePaths.join(','),
|
||||
file: filePaths, // Array of file paths - getApiPath will create repeated parameters
|
||||
algo: format,
|
||||
...(shareHash && { hash: shareHash }),
|
||||
...(!shareHash && source && { source: encodeURIComponent(source) }),
|
||||
@@ -203,9 +203,8 @@ async function downloadChunked(file, shareHash = "") {
|
||||
mutations.showHover({ name: 'download' })
|
||||
}
|
||||
|
||||
// Build the download URL with new query format
|
||||
const params = {
|
||||
files: encodeURIComponent(file.path),
|
||||
file: encodeURIComponent(file.path),
|
||||
...(shareHash && { hash: shareHash }),
|
||||
...(!shareHash && file.source && { source: encodeURIComponent(file.source) }),
|
||||
...(state.share.token && { token: state.share.token }),
|
||||
@@ -518,7 +517,7 @@ export function getDownloadURL(source, path, inline, useExternal) {
|
||||
try {
|
||||
const params = {
|
||||
source: encodeURIComponent(source),
|
||||
files: encodeURIComponent(path),
|
||||
file: encodeURIComponent(path),
|
||||
...(inline && { inline: 'true' })
|
||||
}
|
||||
const apiPath = getApiPath('api/raw', params)
|
||||
|
||||
@@ -59,15 +59,17 @@ export async function fetchPub(path, hash, password = "", content = false, metad
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDownloadURL(share, files, inline=false) {
|
||||
// Join files array with || delimiter and then URL encode
|
||||
const filesParam = Array.isArray(files) ? files.join('||') : files;
|
||||
// Handle array of files for repeated 'file' parameters
|
||||
const fileArray = Array.isArray(files) ? files : [files]
|
||||
const filePaths = fileArray.map(file => encodeURIComponent(file))
|
||||
|
||||
const params = {
|
||||
files: encodeURIComponent(filesParam),
|
||||
file: filePaths, // Array of file paths - getPublicApiPath will create repeated parameters
|
||||
hash: share.hash,
|
||||
token: share.token,
|
||||
...(inline && { inline: 'true' })
|
||||
}
|
||||
const apiPath = getPublicApiPath("raw", params);
|
||||
const apiPath = getPublicApiPath("raw", params)
|
||||
return window.origin + apiPath
|
||||
}
|
||||
|
||||
|
||||
@@ -114,8 +114,7 @@ export default {
|
||||
if (!this.isValNumeric) return 100;
|
||||
if (this.max <= 0) return 0;
|
||||
var pct = (this.val / this.max) * 100;
|
||||
var finalPct = Math.max(0, Math.min(pct.toFixed(2), 100));
|
||||
return finalPct > 0 && finalPct < 7 ? 6 : finalPct;
|
||||
return Math.max(0, Math.min(pct.toFixed(2), 100));
|
||||
},
|
||||
displayed_text() {
|
||||
if (!this.isValNumeric) return this.val;
|
||||
@@ -208,8 +207,12 @@ export default {
|
||||
barColor = '#ff9800';
|
||||
}
|
||||
|
||||
let percentage = this.pct > 1 && this.pct < 7 ? 6 : this.pct;
|
||||
if (percentage < 1) {
|
||||
percentage = 0;
|
||||
}
|
||||
var style = {
|
||||
width: this.pct + "%",
|
||||
width: percentage + "%",
|
||||
height: this.size_px + "px",
|
||||
background: barColor,
|
||||
transition: this.barTransition,
|
||||
|
||||
@@ -304,8 +304,8 @@ export default {
|
||||
return this.selectedSource === "__all__" || (this.selectedSource === "" && this.multipleSources);
|
||||
},
|
||||
getContext() {
|
||||
// If searching all sources, don't use context
|
||||
if (this.selectedSource === "__all__") {
|
||||
// If searching all sources (explicitly or by default), always use root context
|
||||
if (this.selectedSource === "__all__" || (this.selectedSource === "" && this.multipleSources)) {
|
||||
return "/";
|
||||
}
|
||||
let result = url.extractSourceFromPath(decodeURIComponent(state.route.path));
|
||||
@@ -375,7 +375,12 @@ export default {
|
||||
getItemUrl(s) {
|
||||
// Use source from result if available, otherwise fall back to selectedSource
|
||||
const source = s.source || this.selectedSource || state.sources.current;
|
||||
return url.buildItemUrl(source, s.path, true);
|
||||
// Combine context (scope) with the result path - needed when searching within a folder
|
||||
// Ensure exactly one slash between context and path
|
||||
const context = url.removeTrailingSlash(this.getContext);
|
||||
const path = url.removeLeadingSlash(url.removeTrailingSlash(s.path));
|
||||
const fullPath = context + '/' + path;
|
||||
return url.buildItemUrl(source, fullPath, true);
|
||||
},
|
||||
getIcon(mimetype) {
|
||||
return getMaterialIconForType(mimetype);
|
||||
@@ -513,9 +518,13 @@ export default {
|
||||
try {
|
||||
// Use source from result if available, otherwise fall back to selectedSource
|
||||
const source = s.source || this.selectedSource || state.sources.current;
|
||||
const path = s.path;
|
||||
// Combine context (scope) with the result path - needed when searching within a folder
|
||||
// Ensure exactly one slash between context and path
|
||||
const context = url.removeTrailingSlash(this.getContext);
|
||||
const path = url.removeLeadingSlash(url.removeTrailingSlash(s.path));
|
||||
const fullPath = context + '/' + path;
|
||||
const modified = s.modified || "";
|
||||
return filesApi.getPreviewURL(source, path, modified);
|
||||
return filesApi.getPreviewURL(source, fullPath, modified);
|
||||
} catch (err) {
|
||||
return "";
|
||||
}
|
||||
@@ -524,7 +533,10 @@ export default {
|
||||
const pathParts = url.removeTrailingSlash(s.path).split("/");
|
||||
// Use source from result if available, otherwise fall back to selectedSource
|
||||
const source = s.source || this.selectedSource || state.sources.current;
|
||||
let path = this.getContext + url.removeTrailingSlash(s.path);
|
||||
// Combine context (scope) with the result path - ensure exactly one slash between
|
||||
const context = url.removeTrailingSlash(this.getContext);
|
||||
const pathStr = url.removeLeadingSlash(url.removeTrailingSlash(s.path));
|
||||
let path = context + '/' + pathStr;
|
||||
const modifiedItem = {
|
||||
name: pathParts.pop(),
|
||||
path: path,
|
||||
|
||||
@@ -48,6 +48,9 @@ export default {
|
||||
return getters.isStickySidebar() || getters.isShare();
|
||||
},
|
||||
showShelf() {
|
||||
if (state.loading.length > 0) {
|
||||
return false;
|
||||
}
|
||||
return this.showBreadcrumbs || this.showListingHeader || this.showDuplicateFinderActions;
|
||||
},
|
||||
showBreadcrumbs() {
|
||||
|
||||
@@ -237,7 +237,8 @@ export default {
|
||||
return this.readOnly == undefined && state.user.permissions?.modify || state.shareInfo.allowCreate;
|
||||
},
|
||||
canDrop() {
|
||||
if (!this.isDir || this.readOnly !== undefined) return false;
|
||||
if (!this.isDir) return false;
|
||||
if (this.readOnly === true) return false;
|
||||
|
||||
for (const i of this.selected) {
|
||||
if (
|
||||
@@ -309,6 +310,7 @@ export default {
|
||||
this.isInView = true;
|
||||
}
|
||||
});
|
||||
document.addEventListener('dragend', this.dragEnd);
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up observer
|
||||
@@ -316,6 +318,7 @@ export default {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
document.removeEventListener('dragend', this.dragEnd);
|
||||
},
|
||||
methods: {
|
||||
/** @param {MouseEvent} event */
|
||||
@@ -372,6 +375,10 @@ export default {
|
||||
},
|
||||
/** @param {MouseEvent} event */
|
||||
onRightClick(event) {
|
||||
if (!this.updateGlobalState) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.preventDefault(); // Prevent default context menu
|
||||
// If one or fewer items are selected, reset the selection
|
||||
if (this.updateGlobalState) {
|
||||
@@ -472,6 +479,9 @@ export default {
|
||||
event.preventDefault();
|
||||
this.isDraggedOver = true;
|
||||
},
|
||||
dragEnd() {
|
||||
this.isDraggedOver = false;
|
||||
},
|
||||
/** @param {DragEvent} event */
|
||||
async drop(event) {
|
||||
this.isDraggedOver = false;
|
||||
|
||||
@@ -918,7 +918,6 @@ export default {
|
||||
name: "file-list",
|
||||
props: {
|
||||
fileList: currentItems,
|
||||
mode: "quick-jump",
|
||||
title: this.$t("prompts.quickJump")
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div v-if="isDisplayMode" class="card-title">
|
||||
<h2>{{ effectiveTitle }}</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<!-- Source Selection Dropdown -->
|
||||
<div v-if="showSourceSelector" class="source-selector" style="margin-bottom: 1rem;">
|
||||
<div v-if="showSourceSelector && !isDisplayMode" class="source-selector" style="margin-bottom: 1rem;">
|
||||
<label for="destinationSource" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
{{ $t("prompts.destinationSource") }}
|
||||
</label>
|
||||
@@ -13,7 +16,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Current Path Display -->
|
||||
<div aria-label="filelist-path" class="searchContext button clickable">
|
||||
<div v-if="!isDisplayMode" aria-label="filelist-path" class="searchContext button clickable">
|
||||
{{ $t('general.path', { suffix: ':' }) }} {{ sourcePath.path }}
|
||||
</div>
|
||||
|
||||
@@ -47,6 +50,14 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel button for display mode -->
|
||||
<div v-if="isDisplayMode" class="card-action">
|
||||
<button @click="closePrompt" class="button button--flat" :aria-label="$t('general.cancel')"
|
||||
:title="$t('general.cancel')">
|
||||
{{ $t('general.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -71,6 +82,10 @@ export default {
|
||||
type: String,
|
||||
default: null, // Share hash to browse
|
||||
},
|
||||
fileList: {
|
||||
type: Array,
|
||||
default: null, // for example, in "quick jump" we provide the listing from the parent.
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
@@ -135,8 +150,12 @@ export default {
|
||||
// Get all available sources from state.sources.info
|
||||
return state.sources && state.sources.info ? Object.keys(state.sources.info) : [state.req.source];
|
||||
},
|
||||
isDisplayMode() {
|
||||
// Will be in display mode if the fileList prop is provided
|
||||
return this.fileList !== null;
|
||||
},
|
||||
showSourceSelector() {
|
||||
return this.availableSources.length > 1 && !getters.isShare() && !this.browseShare;
|
||||
return this.availableSources.length > 1 && !this.isDisplayMode && !getters.isShare() && !this.browseShare;
|
||||
},
|
||||
isValidSelection() {
|
||||
// If file selection is required, check if a file (not folder) is selected
|
||||
@@ -166,7 +185,12 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.browseShare) {
|
||||
if (this.isDisplayMode) {
|
||||
this.withLoading(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
this.fillFromList();
|
||||
});
|
||||
} else if (this.browseShare) {
|
||||
// Browse a specific share
|
||||
this.withLoading(() => publicApi.fetchPub("/", this.browseShare).then(this.fillOptions));
|
||||
} else {
|
||||
@@ -385,10 +409,15 @@ export default {
|
||||
this.next(event);
|
||||
}
|
||||
},
|
||||
handleItemClick(item, index, event) {
|
||||
handleItemClick(item, _index, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.isDisplayMode) {
|
||||
this.navigateToItem(item);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a synthetic event-like object for compatibility with existing methods
|
||||
const syntheticEvent = {
|
||||
currentTarget: {
|
||||
@@ -406,10 +435,10 @@ export default {
|
||||
this.select(syntheticEvent);
|
||||
}
|
||||
},
|
||||
handleItemDblClick(item, index, event) {
|
||||
handleItemDblClick(item, _index, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
// Create a synthetic event for double-click
|
||||
const syntheticEvent = {
|
||||
currentTarget: {
|
||||
@@ -458,6 +487,18 @@ export default {
|
||||
onSourceChange() {
|
||||
this.resetToSource(this.currentSource);
|
||||
},
|
||||
fillFromList() {
|
||||
// Use the provided fileList, filtering out directories to show only files
|
||||
const allItems = this.fileList || [];
|
||||
this.items = allItems.filter(item => !item.isDirectory && item.type !== 'directory');
|
||||
},
|
||||
navigateToItem(item) {
|
||||
mutations.closeHovers();
|
||||
url.goToItem(item.source || state.req.source, item.path, undefined);
|
||||
},
|
||||
closePrompt() {
|
||||
mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -210,6 +210,17 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Parse the error response structure (similar to Delete.vue)
|
||||
let errorMessage = this.$t("prompts.renameFailed");
|
||||
|
||||
if (error && error.failed && error.failed.length > 0) {
|
||||
// Get message from first failed item
|
||||
errorMessage = error.failed[0].message || errorMessage;
|
||||
} else if (error && error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
notify.showError(errorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "تعذر النسخ إلى الحافظة",
|
||||
"moveFailed": "فشلت عملية النقل",
|
||||
"operationFailed": "فشلت العملية",
|
||||
"partialSuccess": "فشلت بعض العمليات: {succeeded} نجحت ، {failed} فشلت "
|
||||
"partialSuccess": "فشلت بعض العمليات: {succeeded} نجحت ، {failed} فشلت ",
|
||||
"renameFailed": "فشل في إعادة تسمية العنصر"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "أصغر من:",
|
||||
@@ -705,7 +706,6 @@
|
||||
"disableViewingFilesDescription": "قائمة مفصولة بمسافات لامتدادات الملفات لتعطيل عارض/محرر الملفات (على سبيل المثال، \".txt .pdf\").",
|
||||
"disablePreviewExt": "تعطيل معاينات الملفات المكتبية لملحقات ملفات معينة",
|
||||
"disablePreviewExtDescription": "قائمة مفصولة بمسافات لامتدادات الملفات لتعطيل معاينات ملفات المكتب (على سبيل المثال، '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "قائمة مفصولة بمسافات لامتدادات الملفات لتعطيل محرر وعارض OnlyOffice لها. (على سبيل المثال، \".txt .html .pdf\").",
|
||||
"disableOfficeEditor": "تعطيل عارض المكتب لامتدادات ملفات معينة",
|
||||
"defaultThemeDescription": "افتراضي - السمة الافتراضية",
|
||||
"customTheme": "اختر المظهر الخاص بك",
|
||||
@@ -719,7 +719,8 @@
|
||||
"themeAndLanguage": "الموضوع واللغة",
|
||||
"previewFolder": "إظهار الصور المصغرة للمجلدات إذا كانت متوفرة",
|
||||
"previewFolderDescription": "إظهار معاينات الصور المصغرة للمجلدات التي تحتوي على العنصر الفرعي الأول مع صورة مصغرة كغلاف.",
|
||||
"disableFileExtensions": "أدخل امتدادات الملفات لتعطيلها"
|
||||
"disableFileExtensions": "أدخل امتدادات الملفات لتعطيلها",
|
||||
"disableOfficeEditorDescription": "قائمة مفصولة بمسافات لامتدادات الملفات لتعطيل محرر وعارض OnlyOffice - أو '*' لتعطيلها كلها. (على سبيل المثال، \".txt .html .pdf\")."
|
||||
},
|
||||
"colors": {
|
||||
"red": "أحمر",
|
||||
|
||||
@@ -300,7 +300,8 @@
|
||||
"copyToClipboardFailed": "Nelze zkopírovat do schránky",
|
||||
"moveFailed": "Přesun se nezdařil",
|
||||
"operationFailed": "Operace se nezdařila",
|
||||
"partialSuccess": "Některé operace se nezdařily: {succeeded} uspěl, {failed} neuspěl."
|
||||
"partialSuccess": "Některé operace se nezdařily: {succeeded} uspěl, {failed} neuspěl.",
|
||||
"renameFailed": "Nepodařilo se přejmenovat položku"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Menší než:",
|
||||
@@ -361,7 +362,6 @@
|
||||
"disableViewingFilesDescription": "Seznam přípon souborů oddělených mezerou, pro které se má prohlížeč/editor souborů zakázat (např. '.txt .pdf').",
|
||||
"disablePreviewExt": "Zakázat náhledy kancelářských souborů pro určité přípony souborů",
|
||||
"disablePreviewExtDescription": "Seznam přípon souborů oddělených mezerou, pro které chcete zakázat náhledy kancelářských souborů (např. '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Seznam přípon souborů oddělených mezerou, pro které se má vypnout editor a prohlížeč OnlyOffice. (např. '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Zakázat editor a prohlížeč Office pro určité přípony souborů a vrátit se k výchozímu prohlížeči/editoru, velmi užitečné pro soubory pdf a txt.",
|
||||
"defaultThemeDescription": "default - Výchozí téma",
|
||||
"customTheme": "Vyberte si téma",
|
||||
@@ -375,7 +375,8 @@
|
||||
"themeAndLanguage": "Téma a jazyk",
|
||||
"previewFolder": "Zobrazení miniatur složek, pokud jsou k dispozici",
|
||||
"previewFolderDescription": "Zobrazení náhledů miniatur pro složky s první podřízenou položkou s miniaturou jako obálkou.",
|
||||
"disableFileExtensions": "Zadejte přípony souborů, které chcete zakázat"
|
||||
"disableFileExtensions": "Zadejte přípony souborů, které chcete zakázat",
|
||||
"disableOfficeEditorDescription": "Seznam přípon souborů oddělených mezerou, pro které se má editor a prohlížeč OnlyOffice zakázat - nebo '*' pro zakázání všech. (např. '.txt .html .pdf')."
|
||||
},
|
||||
"settings": {
|
||||
"enterPassword": "Zadejte heslo",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Kopieren in die Zwischenablage nicht möglich",
|
||||
"moveFailed": "Verschieben fehlgeschlagen",
|
||||
"operationFailed": "Operation fehlgeschlagen",
|
||||
"partialSuccess": "Einige Operationen sind fehlgeschlagen: {succeeded} erfolgreich, {failed} fehlgeschlagen"
|
||||
"partialSuccess": "Einige Operationen sind fehlgeschlagen: {succeeded} erfolgreich, {failed} fehlgeschlagen",
|
||||
"renameFailed": "Element kann nicht umbenannt werden"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Kleiner als:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "Eine durch Leerzeichen getrennte Liste von Dateierweiterungen, für die der Dateibetrachter/-editor deaktiviert werden soll (z. B. '.txt .pdf').",
|
||||
"disablePreviewExt": "Office-Dateivorschau für bestimmte Dateierweiterungen deaktivieren",
|
||||
"disablePreviewExtDescription": "Eine durch Leerzeichen getrennte Liste von Dateierweiterungen, für die die Office-Dateivorschau deaktiviert werden soll (z. B. '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Eine durch Leerzeichen getrennte Liste von Dateierweiterungen, für die der Editor und der Viewer von OnlyOffice deaktiviert werden sollen. (z. B. '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Office Viewer für bestimmte Dateierweiterungen deaktivieren",
|
||||
"defaultThemeDescription": "default - Das Standardthema",
|
||||
"customTheme": "Design",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Design und Sprache",
|
||||
"previewFolder": "Miniaturansichten für Ordner anzeigen, falls vorhanden",
|
||||
"previewFolderDescription": "Zeigen Sie Miniaturvorschauen für Ordner an, deren erstes untergeordnetes Element ein Miniaturbild als Cover hat.",
|
||||
"disableFileExtensions": "Zu deaktivierende Dateierweiterungen eingeben"
|
||||
"disableFileExtensions": "Zu deaktivierende Dateierweiterungen eingeben",
|
||||
"disableOfficeEditorDescription": "Eine durch Leerzeichen getrennte Liste von Dateierweiterungen, für die der OnlyOffice-Editor und -Betrachter deaktiviert werden soll - oder '*', um ihn für alle zu deaktivieren. (z. B. '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Der Editor konnte nicht initialisiert werden. Bitte neu laden.",
|
||||
|
||||
@@ -226,7 +226,8 @@
|
||||
"copyToClipboardFailed": "Αδυναμία αντιγραφής στο πρόχειρο",
|
||||
"moveFailed": "Η μετακίνηση απέτυχε",
|
||||
"operationFailed": "Η λειτουργία απέτυχε",
|
||||
"partialSuccess": "Ορισμένες λειτουργίες απέτυχαν: {succeeded} πέτυχε, {failed} απέτυχε"
|
||||
"partialSuccess": "Ορισμένες λειτουργίες απέτυχαν: {succeeded} πέτυχε, {failed} απέτυχε",
|
||||
"renameFailed": "Απέτυχε η μετονομασία στοιχείου"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Μικρότερο από:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "Μια λίστα με διαχωρισμό διαστημάτων των επεκτάσεων αρχείων για τις οποίες θα απενεργοποιηθεί η προβολή/επεξεργασία αρχείων (π.χ., '.txt .pdf').",
|
||||
"disablePreviewExt": "Απενεργοποίηση προεπισκοπήσεων αρχείων γραφείου για ορισμένες επεκτάσεις αρχείων",
|
||||
"disablePreviewExtDescription": "Μια λίστα επεκτάσεων αρχείων με διαχωρισμό διαστήματος για την απενεργοποίηση της προεπισκόπησης αρχείων γραφείου (π.χ. '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Μια λίστα επεκτάσεων αρχείων με διαχωρισμό διαστήματος για την απενεργοποίηση του επεξεργαστή και της προβολής του OnlyOffice. (π.χ. '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Απενεργοποίηση της προβολής γραφείου για ορισμένες επεκτάσεις αρχείων",
|
||||
"defaultThemeDescription": "default - Το προεπιλεγμένο θέμα",
|
||||
"customTheme": "Επιλέξτε το θέμα σας",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Θέμα & Γλώσσα",
|
||||
"previewFolder": "Εμφάνιση μικρογραφιών για φακέλους, εάν είναι διαθέσιμες",
|
||||
"previewFolderDescription": "Εμφάνιση προεπισκοπήσεων μικρογραφιών για φακέλους με το πρώτο στοιχείο-παιδί με μικρογραφία ως εξώφυλλο.",
|
||||
"disableFileExtensions": "Εισάγετε επεκτάσεις αρχείων για απενεργοποίηση"
|
||||
"disableFileExtensions": "Εισάγετε επεκτάσεις αρχείων για απενεργοποίηση",
|
||||
"disableOfficeEditorDescription": "Μια λίστα επεκτάσεων αρχείων με διαχωρισμό διαστήματος για την απενεργοποίηση του προγράμματος επεξεργασίας και προβολής OnlyOffice - ή '*' για την απενεργοποίηση όλων. (π.χ. '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Απέτυχε η αρχικοποίηση του συντάκτη. Παρακαλούμε επαναφορτώστε.",
|
||||
|
||||
@@ -373,6 +373,7 @@
|
||||
"renameMessageConflict": "An item named \"{filename}\" already exists!",
|
||||
"renameMessage": "Enter a new name:",
|
||||
"renameMessageInvalid": "A file can't container \"/\" or \"\\\"",
|
||||
"renameFailed": "Failed to rename item",
|
||||
"replace": "Replace",
|
||||
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to continue to upload or replace the existing one?\n",
|
||||
"saveBeforeExit": "Unsaved Changes",
|
||||
@@ -490,7 +491,7 @@
|
||||
"disableViewingFilesDescription": "A space-separated list of file extensions to disable the file viewer/editor for (e.g., '.txt .pdf').",
|
||||
"disablePreviewExt": "Disable office file previews for certain file extensions",
|
||||
"disablePreviewExtDescription": "A space-separated list of file extensions to disable office file previews for (e.g., '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "A space-separated list of file extensions to disable the OnlyOffice editor and viewer for. (e.g., '.txt .html .pdf').",
|
||||
"disableOfficeEditorDescription": "A space-separated list of file extensions to disable the OnlyOffice editor and viewer for - or '*' to disable for all. (e.g., '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Disable office viewer for certain file extensions",
|
||||
"fileViewerOptions": "File Viewer Options",
|
||||
"themeAndLanguage": "Theme & Language"
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Hubo un error al copiar en el portapapeles",
|
||||
"moveFailed": "Error al mover",
|
||||
"operationFailed": "Operación fallida",
|
||||
"partialSuccess": "Algunas operaciones fallaron: {succeeded} tuvo éxito, {failed} falló"
|
||||
"partialSuccess": "Algunas operaciones fallaron: {succeeded} tuvo éxito, {failed} falló",
|
||||
"renameFailed": "Error al renombrar elemento"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Menor que:",
|
||||
@@ -684,7 +685,6 @@
|
||||
"disableViewingFilesDescription": "Una lista (separada por espacios) de extensiones de archivo para desactivar el visor/editor de archivos (por ejemplo, '.txt .pdf').",
|
||||
"disablePreviewExt": "Desactivar la vista previa de archivos de Office para las siguientes extensiones:",
|
||||
"disablePreviewExtDescription": "Una lista (separada por espacios) de las extensiones de archivo para las que se desactivará la vista previa de los archivos de Office (por ejemplo, '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Una lista (separada por espacios) de las extensiones de archivo para las que desea desactivar el editor y el visor de OnlyOffice. (por ejemplo, '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Desactivar el visor y editor de office para las siguientes extensiones:",
|
||||
"defaultThemeDescription": "Predeterminado",
|
||||
"customTheme": "Cambiar tema",
|
||||
@@ -727,7 +727,8 @@
|
||||
"fileViewerOptions": "Opciones del visor de archivos",
|
||||
"themeAndLanguage": "Tema e Idioma",
|
||||
"previewFolder": "Vista previa en miniatura para carpetas",
|
||||
"previewFolderDescription": "Mostrar vistas previas en miniatura para las carpetas - Se usará el primer elemento como portada. (puede ser una imagen, o audio con imagen)"
|
||||
"previewFolderDescription": "Mostrar vistas previas en miniatura para las carpetas - Se usará el primer elemento como portada. (puede ser una imagen, o audio con imagen)",
|
||||
"disableOfficeEditorDescription": "Una lista separada por espacios de extensiones de archivo para las que desactivar el editor y visor de OnlyOffice, o '*' para desactivarlas todas. (por ejemplo, '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Hubo un error al iniciar el editor",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Impossible de copier dans le presse-papiers",
|
||||
"moveFailed": "Échec du déplacement",
|
||||
"operationFailed": "Échec de l'opération",
|
||||
"partialSuccess": "Certaines opérations ont échoué : {succeeded} a réussi, {failed} a échoué"
|
||||
"partialSuccess": "Certaines opérations ont échoué : {succeeded} a réussi, {failed} a échoué",
|
||||
"renameFailed": "Échec du renommage de l'élément"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Plus petit que :",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "Une liste d'extensions de fichiers séparées par des espaces pour lesquelles le visualiseur/éditeur de fichiers doit être désactivé (par exemple, '.txt .pdf').",
|
||||
"disablePreviewExt": "Désactiver les aperçus de fichiers Office pour certaines extensions de fichiers",
|
||||
"disablePreviewExtDescription": "Une liste d'extensions de fichiers séparées par des espaces pour lesquelles les aperçus de fichiers Office doivent être désactivés (par exemple, '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Une liste d'extensions de fichiers séparées par des espaces pour lesquelles l'éditeur et la visionneuse OnlyOffice doivent être désactivés. (par exemple, '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Désactiver la visionneuse d'office pour certaines extensions de fichiers",
|
||||
"defaultThemeDescription": "default - Le thème par défaut",
|
||||
"customTheme": "Choisissez votre thème",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Thème et langue",
|
||||
"previewFolder": "Afficher les vignettes des dossiers si elles sont disponibles",
|
||||
"previewFolderDescription": "Afficher des aperçus de vignettes pour les dossiers dont le premier élément enfant a une vignette comme couverture.",
|
||||
"disableFileExtensions": "Saisir les extensions de fichiers à désactiver"
|
||||
"disableFileExtensions": "Saisir les extensions de fichiers à désactiver",
|
||||
"disableOfficeEditorDescription": "Une liste d'extensions de fichiers séparées par des espaces pour lesquelles l'éditeur et la visionneuse OnlyOffice doivent être désactivés - ou '*' pour les désactiver toutes. (par exemple, '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Échec de l'initialisation de l'éditeur. Veuillez recharger.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "לא ניתן להעתיק ללוח",
|
||||
"moveFailed": "העברה נכשלה",
|
||||
"operationFailed": "הפעולה נכשלה",
|
||||
"partialSuccess": "כמה פעולות נכשלו: {succeeded} הצליחו, {failed} נכשלו"
|
||||
"partialSuccess": "כמה פעולות נכשלו: {succeeded} הצליחו, {failed} נכשלו",
|
||||
"renameFailed": "לא ניתן לשנות את שם הפריט"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "קטן מ:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "רשימה מופרדת ברווחים של סיומות קבצים שיש להשבית את תצוגת/עורך הקבצים עבורן (לדוגמה, '.txt .pdf').",
|
||||
"disablePreviewExt": "השבת תצוגה מקדימה של קבצי Office עבור סיומות קבצים מסוימות",
|
||||
"disablePreviewExtDescription": "רשימה מופרדת ברווחים של סיומות קבצים שצריך להשבית את תצוגה מקדימה של קבצי Office עבורן (לדוגמה, '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "רשימה מופרדת ברווחים של סיומות קבצים עבורן יש להשבית את עורך ומציג OnlyOffice. (לדוגמה, '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "השבת תצוגת Office עבור סיומות קבצים מסוימות",
|
||||
"defaultThemeDescription": "ברירת מחדל - ערכת הנושא המוגדרת כברירת מחדל",
|
||||
"customTheme": "בחר את הנושא שלך",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "נושא ושפה",
|
||||
"previewFolder": "הצג תמונות ממוזערות של תיקיות, אם זמינות",
|
||||
"previewFolderDescription": "הצג תצוגה מקדימה של תמונות ממוזערות עבור תיקיות עם פריט משנה ראשון עם תמונה ממוזערת ככריכה.",
|
||||
"disableFileExtensions": "הזן סיומות קבצים שיש להשבית"
|
||||
"disableFileExtensions": "הזן סיומות קבצים שיש להשבית",
|
||||
"disableOfficeEditorDescription": "רשימה מופרדת ברווחים של סיומות קבצים להשבית את עורך ומציג OnlyOffice עבור - או '*' להשבית עבור כולם. (לדוגמה, '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "לא ניתן לאתחל את העורך. אנא טען מחדש.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Nem lehet a vágólapra másolni",
|
||||
"moveFailed": "Áthelyezés sikertelen",
|
||||
"operationFailed": "A művelet sikertelen",
|
||||
"partialSuccess": "Néhány művelet sikertelen: {succeeded} sikeres volt, {failed} sikertelen."
|
||||
"partialSuccess": "Néhány művelet sikertelen: {succeeded} sikeres volt, {failed} sikertelen.",
|
||||
"renameFailed": "Nem sikerült átnevezni az elemet"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Kisebb, mint:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "A fájlmegjelenítő/szerkesztő letiltásának fájlkiterjesztések szóközzel elválasztott listája (pl. '.txt .pdf').",
|
||||
"disablePreviewExt": "Office-fájlok előnézetének letiltása bizonyos fájlkiterjesztések esetében",
|
||||
"disablePreviewExtDescription": "Az irodai fájlok előnézetének letiltására szolgáló fájlkiterjesztések szóközzel elválasztott listája (pl. '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Az OnlyOffice szerkesztő és megjelenítő letiltandó fájlkiterjesztések szóközzel elválasztott listája. (pl. \".txt .html .pdf\").",
|
||||
"disableOfficeEditor": "Bizonyos fájlkiterjesztéseknél letilthatja az Office Viewer-t",
|
||||
"defaultThemeDescription": "default - Az alapértelmezett téma",
|
||||
"customTheme": "Válasszon témát",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Téma és nyelv",
|
||||
"previewFolder": "A mappák miniatűrjeinek megjelenítése, ha rendelkezésre állnak",
|
||||
"previewFolderDescription": "Miniatűr előnézetek megjelenítése olyan mappákhoz, amelyekben az első gyermekelem miniatűrrel borítóképként szerepel.",
|
||||
"disableFileExtensions": "Adja meg a letiltandó fájlkiterjesztéseket"
|
||||
"disableFileExtensions": "Adja meg a letiltandó fájlkiterjesztéseket",
|
||||
"disableOfficeEditorDescription": "Az OnlyOffice szerkesztő és megjelenítő program letiltásának szóközzel elválasztott listája - vagy '*' az összes fájlkiterjesztés letiltásához. (pl. \".txt .html .pdf\")."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nem sikerült inicializálni a szerkesztőt. Kérjük, töltse újra.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Impossibile copiare negli appunti",
|
||||
"moveFailed": "Spostamento non riuscito",
|
||||
"operationFailed": "Operazione fallita",
|
||||
"partialSuccess": "Alcune operazioni non sono riuscite: {succeeded} è riuscito, {failed} non è riuscito."
|
||||
"partialSuccess": "Alcune operazioni non sono riuscite: {succeeded} è riuscito, {failed} non è riuscito.",
|
||||
"renameFailed": "Impossibile rinominare l'elemento"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Più piccolo di:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "Un elenco separato da spazi di estensioni di file per cui disabilitare il visualizzatore/editor di file (ad esempio, '.txt .pdf').",
|
||||
"disablePreviewExt": "Disattivare le anteprime dei file di Office per alcune estensioni di file",
|
||||
"disablePreviewExtDescription": "Un elenco separato da spazi di estensioni di file per cui disabilitare le anteprime dei file di Office (ad esempio, '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Un elenco separato da spazi di estensioni di file per cui disabilitare l'editor e il visualizzatore di OnlyOffice. (ad esempio, '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Disattivare il visualizzatore di Office per alcune estensioni di file",
|
||||
"defaultThemeDescription": "default - Il tema predefinito",
|
||||
"customTheme": "Scegliete il vostro tema",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Tema e linguaggio",
|
||||
"previewFolder": "Mostra le miniature delle cartelle, se disponibili.",
|
||||
"previewFolderDescription": "Mostra le anteprime per le cartelle con il primo elemento figlio con miniatura come copertina.",
|
||||
"disableFileExtensions": "Immettere le estensioni dei file da disabilitare"
|
||||
"disableFileExtensions": "Immettere le estensioni dei file da disabilitare",
|
||||
"disableOfficeEditorDescription": "Un elenco, separato da spazi, di estensioni di file per cui disabilitare l'editor e il visualizzatore di OnlyOffice, oppure '*' per disabilitarli tutti. (ad esempio, '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Impossibile inizializzare l'editor. Ricaricare.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "クリップボードにコピーできない",
|
||||
"moveFailed": "移動に失敗しました",
|
||||
"operationFailed": "操作失敗",
|
||||
"partialSuccess": "{succeeded} は成功、{failed} は失敗。"
|
||||
"partialSuccess": "{succeeded} は成功、{failed} は失敗。",
|
||||
"renameFailed": "アイテム名の変更に失敗しました"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "より小さい:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "ファイルビューア/エディタを無効にするファイル拡張子のスペース区切りリスト(例:'.txt .pdf')",
|
||||
"disablePreviewExt": "特定のファイル拡張子でオフィスファイルのプレビューを無効にする",
|
||||
"disablePreviewExtDescription": "オフィスファイルのプレビューを無効にするファイル拡張子をスペースで区切ったリスト(例:'.docx .pptx')",
|
||||
"disableOfficeEditorDescription": "OnlyOfficeエディタおよびビューアを無効にするファイル拡張子のスペース区切りリスト(例:'.txt .html .pdf')",
|
||||
"disableOfficeEditor": "特定のファイル拡張子のオフィスビューアを無効にする",
|
||||
"defaultThemeDescription": "default - デフォルトテーマ",
|
||||
"customTheme": "テーマを選択",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "テーマと言語",
|
||||
"previewFolder": "利用可能であれば、フォルダのサムネイルを表示する",
|
||||
"previewFolderDescription": "サムネイルを持つ最初の子アイテムをカバーとするフォルダのサムネイルプレビューを表示します。",
|
||||
"disableFileExtensions": "無効にするファイル拡張子を入力"
|
||||
"disableFileExtensions": "無効にするファイル拡張子を入力",
|
||||
"disableOfficeEditorDescription": "OnlyOfficeエディタおよびビューアを無効にするファイル拡張子をスペース区切りで指定します。(例: '.txt .html .pdf')。"
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "エディタの初期化に失敗しました。再読み込みしてください。",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "클립보드에 복사할 수 없습니다.",
|
||||
"moveFailed": "이동 실패",
|
||||
"operationFailed": "작업 실패",
|
||||
"partialSuccess": "일부 작업 실패: {succeeded} 성공, {failed} 실패"
|
||||
"partialSuccess": "일부 작업 실패: {succeeded} 성공, {failed} 실패",
|
||||
"renameFailed": "항목 이름 변경에 실패했습니다."
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "보다 작습니다:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "파일 뷰어/편집기를 비활성화할 파일 확장명 목록(예: '.txt .pdf')을 공백으로 구분하여 표시합니다.",
|
||||
"disablePreviewExt": "특정 파일 확장명에 대한 Office 파일 미리 보기 비활성화",
|
||||
"disablePreviewExtDescription": "Office 파일 미리 보기를 사용하지 않도록 설정할 파일 확장명 목록(예: '.docx .pptx')을 공백으로 구분하여 표시합니다.",
|
||||
"disableOfficeEditorDescription": "OnlyOffice 편집기 및 뷰어를 비활성화할 파일 확장명을 공백으로 구분한 목록입니다. (예: '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "특정 파일 확장자에 대해 Office 뷰어 사용 안 함",
|
||||
"defaultThemeDescription": "default - 기본 테마",
|
||||
"customTheme": "테마 선택",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "테마 및 언어",
|
||||
"previewFolder": "가능한 경우 폴더의 썸네일 표시",
|
||||
"previewFolderDescription": "썸네일을 표지로 사용하는 첫 번째 하위 항목이 있는 폴더의 썸네일 미리 보기를 표시합니다.",
|
||||
"disableFileExtensions": "비활성화할 파일 확장명을 입력합니다."
|
||||
"disableFileExtensions": "비활성화할 파일 확장명을 입력합니다.",
|
||||
"disableOfficeEditorDescription": "공백으로 구분된 파일 확장명 목록으로, OnlyOffice 편집기 및 뷰어를 비활성화하거나 모두 비활성화하려면 '*'를 입력합니다. (예: '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "편집기를 초기화하지 못했습니다. 다시 로드하세요.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Kan niet naar klembord kopiëren",
|
||||
"moveFailed": "Verplaatsen mislukt",
|
||||
"operationFailed": "Operatie mislukt",
|
||||
"partialSuccess": "Sommige bewerkingen zijn mislukt: {succeeded} gelukt, {failed} mislukt."
|
||||
"partialSuccess": "Sommige bewerkingen zijn mislukt: {succeeded} gelukt, {failed} mislukt.",
|
||||
"renameFailed": "Hernoemen van item mislukt"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Kleiner dan:",
|
||||
@@ -580,7 +581,6 @@
|
||||
"disableViewingFilesDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor de bestandsviewer/editor uitgeschakeld moet worden (bijvoorbeeld '.txt .pdf').",
|
||||
"disablePreviewExt": "Office-bestandsvoorvertoningen uitschakelen voor bepaalde bestandsextensies",
|
||||
"disablePreviewExtDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor Office-bestandsvoorvertoningen moeten worden uitgeschakeld (bijvoorbeeld '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor de OnlyOffice editor en viewer uitgeschakeld moeten worden. (bijv. '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Office Viewer uitschakelen voor bepaalde bestandsextensies",
|
||||
"defaultThemeDescription": "standaard - Het standaardthema",
|
||||
"customTheme": "Kies je thema",
|
||||
@@ -594,7 +594,8 @@
|
||||
"themeAndLanguage": "Thema & Taal",
|
||||
"previewFolder": "Toon miniaturen voor mappen indien beschikbaar",
|
||||
"previewFolderDescription": "Toon miniatuurafbeeldingen voor mappen met het eerste kinditem met miniatuurafbeelding als omslag.",
|
||||
"disableFileExtensions": "Voer bestandsextensies in om uit te schakelen"
|
||||
"disableFileExtensions": "Voer bestandsextensies in om uit te schakelen",
|
||||
"disableOfficeEditorDescription": "Een door spaties gescheiden lijst van bestandsextensies waarvoor de OnlyOffice editor en viewer uitgeschakeld moeten worden - of '*' om alle extensies uit te schakelen. (bijv. '.txt .html .pdf')."
|
||||
},
|
||||
"general": {
|
||||
"name": "Naam",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Nie można skopiować do schowka",
|
||||
"moveFailed": "Przenoszenie nie powiodło się",
|
||||
"operationFailed": "Operacja nie powiodła się",
|
||||
"partialSuccess": "Niektóre operacje nie powiodły się: {succeeded} powiodło się, {failed} nie powiodło się"
|
||||
"partialSuccess": "Niektóre operacje nie powiodły się: {succeeded} powiodło się, {failed} nie powiodło się",
|
||||
"renameFailed": "Nie udało się zmienić nazwy elementu"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Mniejszy niż:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "Rozdzielana spacjami lista rozszerzeń plików, dla których ma zostać wyłączona przeglądarka/edytor plików (np. \".txt .pdf\").",
|
||||
"disablePreviewExt": "Wyłącz podgląd plików pakietu Office dla niektórych rozszerzeń plików",
|
||||
"disablePreviewExtDescription": "Rozdzielana spacjami lista rozszerzeń plików do wyłączenia podglądu plików biurowych (np. \".docx .pptx\").",
|
||||
"disableOfficeEditorDescription": "Rozdzielana spacjami lista rozszerzeń plików, dla których edytor i przeglądarka OnlyOffice mają zostać wyłączone. (np. \".txt .html .pdf\").",
|
||||
"disableOfficeEditor": "Wyłącz przeglądarkę Office dla niektórych rozszerzeń plików",
|
||||
"defaultThemeDescription": "default - domyślny motyw",
|
||||
"customTheme": "Wybierz motyw",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Temat i język",
|
||||
"previewFolder": "Wyświetlanie miniatur folderów, jeśli są dostępne.",
|
||||
"previewFolderDescription": "Wyświetlanie podglądu miniatur dla folderów z pierwszym elementem podrzędnym z miniaturą jako okładką.",
|
||||
"disableFileExtensions": "Wprowadź rozszerzenia plików do wyłączenia"
|
||||
"disableFileExtensions": "Wprowadź rozszerzenia plików do wyłączenia",
|
||||
"disableOfficeEditorDescription": "Rozdzielana spacjami lista rozszerzeń plików do wyłączenia edytora i przeglądarki OnlyOffice - lub \"*\", aby wyłączyć dla wszystkich. (np. '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nie udało się zainicjować edytora. Przeładuj ponownie.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Não é possível copiar para a área de transferência",
|
||||
"moveFailed": "Falha ao mover",
|
||||
"operationFailed": "Falha na operação",
|
||||
"partialSuccess": "Algumas operações falharam: {succeeded} foi bem-sucedido, {failed} falhou"
|
||||
"partialSuccess": "Algumas operações falharam: {succeeded} foi bem-sucedido, {failed} falhou",
|
||||
"renameFailed": "Falha ao renomear o item"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Menor que:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "Uma lista de extensões de arquivo separada por espaços para desativar o visualizador/editor de arquivos (por exemplo, '.txt .pdf').",
|
||||
"disablePreviewExt": "Desativar as visualizações prévias de arquivos do Office para determinadas extensões de arquivos",
|
||||
"disablePreviewExtDescription": "Uma lista de extensões de arquivo separada por espaços para desativar as visualizações de arquivos do Office (por exemplo, '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Uma lista de extensões de arquivo separada por espaços para desativar o editor e o visualizador do OnlyOffice. (por exemplo, '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Desativar o visualizador do Office para determinadas extensões de arquivo",
|
||||
"defaultThemeDescription": "default - O tema padrão",
|
||||
"customTheme": "Escolha seu tema",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Tema e linguagem",
|
||||
"previewFolder": "Mostrar miniaturas das pastas, se disponíveis",
|
||||
"previewFolderDescription": "Mostrar visualizações em miniatura para pastas com o primeiro item filho com miniatura como capa.",
|
||||
"disableFileExtensions": "Digite as extensões de arquivo a serem desativadas"
|
||||
"disableFileExtensions": "Digite as extensões de arquivo a serem desativadas",
|
||||
"disableOfficeEditorDescription": "Uma lista de extensões de arquivo separada por espaços para desativar o editor e o visualizador do OnlyOffice - ou '*' para desativar todos. (por exemplo, '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Falha ao inicializar o editor. Favor recarregar.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Não é possível copiar para a área de transferência",
|
||||
"moveFailed": "Falha ao mover",
|
||||
"operationFailed": "A operação falhou",
|
||||
"partialSuccess": "Algumas operações falharam: {succeeded} foi bem sucedido, {failed} falhou"
|
||||
"partialSuccess": "Algumas operações falharam: {succeeded} foi bem sucedido, {failed} falhou",
|
||||
"renameFailed": "Falha ao renomear o item"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Mais pequeno do que:",
|
||||
@@ -580,7 +581,6 @@
|
||||
"disableViewingFilesDescription": "Uma lista de extensões de ficheiros, separada por espaços, para desativar o visualizador/editor de ficheiros (por exemplo, '.txt .pdf').",
|
||||
"disablePreviewExt": "Desativar as pré-visualizações de ficheiros do Office para determinadas extensões de ficheiros",
|
||||
"disablePreviewExtDescription": "Uma lista de extensões de ficheiros, separada por espaços, para desativar as pré-visualizações de ficheiros do Office (por exemplo, '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Uma lista de extensões de ficheiros separada por espaços para desativar o editor e visualizador do OnlyOffice. (por exemplo, '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Desativar o visualizador do Office para determinadas extensões de ficheiros",
|
||||
"defaultThemeDescription": "default - O tema predefinido",
|
||||
"customTheme": "Escolha o seu tema",
|
||||
@@ -594,7 +594,8 @@
|
||||
"themeAndLanguage": "Tema e linguagem",
|
||||
"previewFolder": "Mostrar miniaturas das pastas, se disponíveis",
|
||||
"previewFolderDescription": "Mostrar pré-visualizações em miniatura para pastas com o primeiro item filho com miniatura como capa.",
|
||||
"disableFileExtensions": "Introduzir as extensões de ficheiros a desativar"
|
||||
"disableFileExtensions": "Introduzir as extensões de ficheiros a desativar",
|
||||
"disableOfficeEditorDescription": "Uma lista de extensões de ficheiros separada por espaços para desativar o editor e visualizador do OnlyOffice - ou '*' para desativar para todos. (por exemplo, '.txt .html .pdf')."
|
||||
},
|
||||
"general": {
|
||||
"name": "Nome",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Imposibilitatea de a copia în clipboard",
|
||||
"moveFailed": "Mutarea a eșuat",
|
||||
"operationFailed": "Operațiunea a eșuat",
|
||||
"partialSuccess": "Unele operațiuni au eșuat: {succeeded} a reușit, {failed} a eșuat"
|
||||
"partialSuccess": "Unele operațiuni au eșuat: {succeeded} a reușit, {failed} a eșuat",
|
||||
"renameFailed": "A eșuat redenumirea elementului"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Mai mic decât:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "O listă de extensii de fișiere separate prin spațiu pentru care se dezactivează vizualizatorul/editorul de fișiere (de exemplu, '.txt .pdf').",
|
||||
"disablePreviewExt": "Dezactivați previzualizarea fișierelor office pentru anumite extensii de fișiere",
|
||||
"disablePreviewExtDescription": "O listă de extensii de fișiere separate prin spațiu pentru care se dezactivează previzualizările fișierelor office (de exemplu, '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "O listă de extensii de fișiere separate prin spațiu pentru care se dezactivează editorul și vizualizatorul OnlyOffice. (de exemplu, \".txt .html .pdf\").",
|
||||
"disableOfficeEditor": "Dezactivați vizualizatorul office pentru anumite extensii de fișiere",
|
||||
"defaultThemeDescription": "default - Tema implicită",
|
||||
"customTheme": "Alege-ți tema",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Temă și limbaj",
|
||||
"previewFolder": "Afișați miniaturi pentru foldere, dacă sunt disponibile",
|
||||
"previewFolderDescription": "Afișează previzualizări ale miniaturilor pentru folderele cu primul element copil cu miniatură ca copertă.",
|
||||
"disableFileExtensions": "Introduceți extensiile de fișier pe care doriți să le dezactivați"
|
||||
"disableFileExtensions": "Introduceți extensiile de fișier pe care doriți să le dezactivați",
|
||||
"disableOfficeEditorDescription": "O listă de extensii de fișiere separate prin spațiu pentru care se dezactivează editorul și vizualizatorul OnlyOffice - sau \"*\" pentru dezactivarea pentru toate. (de exemplu, \".txt .html .pdf\")."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nu s-a reușit inițializarea editorului. Vă rugăm să reîncărcați.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Невозможно скопировать в буфер обмена",
|
||||
"moveFailed": "Перемещение не удалось",
|
||||
"operationFailed": "Операция не удалась",
|
||||
"partialSuccess": "Некоторые операции завершились неудачей: {succeeded} - успешно, {failed} - неудачно."
|
||||
"partialSuccess": "Некоторые операции завершились неудачей: {succeeded} - успешно, {failed} - неудачно.",
|
||||
"renameFailed": "Не удалось переименовать элемент"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Меньше, чем:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "Список расширений файлов, для которых необходимо отключить просмотрщик/редактор файлов (например, '.txt .pdf'), разделенный пробелами.",
|
||||
"disablePreviewExt": "Отключение предварительного просмотра офисных файлов для определенных расширений файлов",
|
||||
"disablePreviewExtDescription": "Список расширений файлов, для которых нужно отключить предварительный просмотр офисных файлов (например, '.docx .pptx'), разделенный пробелами.",
|
||||
"disableOfficeEditorDescription": "Список расширений файлов, для которых необходимо отключить редактор и просмотрщик OnlyOffice, разделенный пробелами. (например, '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Отключение просмотра офисных файлов для определенных расширений файлов",
|
||||
"defaultThemeDescription": "default - Тема по умолчанию",
|
||||
"customTheme": "Выберите тему",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Тема и язык",
|
||||
"previewFolder": "Показывать миниатюры для папок, если они доступны",
|
||||
"previewFolderDescription": "Показывать предварительный просмотр миниатюр для папок, в которых первый дочерний элемент с миниатюрой является обложкой.",
|
||||
"disableFileExtensions": "Введите расширения файлов, которые необходимо отключить"
|
||||
"disableFileExtensions": "Введите расширения файлов, которые необходимо отключить",
|
||||
"disableOfficeEditorDescription": "Список расширений файлов, для которых необходимо отключить редактор и просмотрщик OnlyOffice, разделенный пробелами, или '*', чтобы отключить все расширения. (например, '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Не удалось инициализировать редактор. Пожалуйста, перезагрузитесь.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Nemožnosť kopírovania do schránky",
|
||||
"moveFailed": "Presun zlyhal",
|
||||
"operationFailed": "Operácia zlyhala",
|
||||
"partialSuccess": "Niektoré operácie zlyhali: {succeeded} sa podarilo, {failed} sa nepodarilo"
|
||||
"partialSuccess": "Niektoré operácie zlyhali: {succeeded} sa podarilo, {failed} sa nepodarilo",
|
||||
"renameFailed": "Nepodarilo sa premenovať položku"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Menšie ako:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "Zoznam prípon súborov oddelených medzerou, pre ktoré sa má prehliadač/editor súborov vypnúť (napr. \".txt .pdf\").",
|
||||
"disablePreviewExt": "Zakázanie náhľadov kancelárskych súborov pre určité prípony súborov",
|
||||
"disablePreviewExtDescription": "Zoznam prípon súborov oddelených medzerou, pre ktoré sa majú vypnúť náhľady kancelárskych súborov (napr. \".docx .pptx\").",
|
||||
"disableOfficeEditorDescription": "Medzerou oddelený zoznam prípon súborov, pre ktoré sa má vypnúť editor a prehliadač OnlyOffice. (napr. \".txt .html .pdf\").",
|
||||
"disableOfficeEditor": "Zakázanie prehliadača Office pre určité prípony súborov",
|
||||
"defaultThemeDescription": "default - Predvolená téma",
|
||||
"customTheme": "Vyberte si tému",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Téma a jazyk",
|
||||
"previewFolder": "Zobrazenie miniatúr priečinkov, ak sú k dispozícii",
|
||||
"previewFolderDescription": "Zobrazenie náhľadov miniatúr pre priečinky s prvou podriadenou položkou s miniatúrou ako obálkou.",
|
||||
"disableFileExtensions": "Zadajte prípony súborov, ktoré chcete zakázať"
|
||||
"disableFileExtensions": "Zadajte prípony súborov, ktoré chcete zakázať",
|
||||
"disableOfficeEditorDescription": "Medzerou oddelený zoznam prípon súborov, pre ktoré sa má zakázať editor a prehliadač OnlyOffice - alebo '*' pre zakázanie pre všetky. (napr. \".txt .html .pdf\")."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nepodarilo sa inicializovať editor. Prosím, načítajte ho znova.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Det går inte att kopiera till urklipp",
|
||||
"moveFailed": "Flyttning misslyckades",
|
||||
"operationFailed": "Operationen misslyckades",
|
||||
"partialSuccess": "Vissa operationer misslyckades: {succeeded} lyckades, {failed} misslyckades"
|
||||
"partialSuccess": "Vissa operationer misslyckades: {succeeded} lyckades, {failed} misslyckades",
|
||||
"renameFailed": "Misslyckades med att byta namn på objekt"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Mindre än:",
|
||||
@@ -580,7 +581,6 @@
|
||||
"disableViewingFilesDescription": "En mellanslagsseparerad lista över filändelser för vilka filvisaren/-redigeraren ska inaktiveras (t.ex. '.txt .pdf').",
|
||||
"disablePreviewExt": "Avaktivera förhandsgranskning av Office-filer för vissa filtillägg",
|
||||
"disablePreviewExtDescription": "En mellanslagsseparerad lista över filtillägg för vilka förhandsgranskningar av Office-filer ska inaktiveras (t.ex. '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "En mellanslagsseparerad lista över filtillägg som OnlyOffice redigerare och visare ska inaktiveras för. (t.ex. '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Avaktivera Office Viewer för vissa filtillägg",
|
||||
"defaultThemeDescription": "default - Standardtema",
|
||||
"customTheme": "Välj ditt tema",
|
||||
@@ -594,7 +594,8 @@
|
||||
"themeAndLanguage": "Tema & språk",
|
||||
"previewFolder": "Visa miniatyrbilder för mappar om de är tillgängliga",
|
||||
"previewFolderDescription": "Visa förhandsvisningar av miniatyrbilder för mappar med första underordnade objekt med miniatyrbild som omslag.",
|
||||
"disableFileExtensions": "Ange filändelser som ska inaktiveras"
|
||||
"disableFileExtensions": "Ange filändelser som ska inaktiveras",
|
||||
"disableOfficeEditorDescription": "En mellanslagsseparerad lista med filändelser som OnlyOffice redigerare och visare ska inaktiveras för - eller '*' för att inaktivera alla. (t.ex. '.txt .html .pdf')."
|
||||
},
|
||||
"general": {
|
||||
"name": "Namn",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Panoya kopyalanamıyor",
|
||||
"moveFailed": "Taşıma başarısız",
|
||||
"operationFailed": "İşlem başarısız oldu",
|
||||
"partialSuccess": "Bazı işlemler başarısız oldu: {succeeded} başarılı, {failed} başarısız"
|
||||
"partialSuccess": "Bazı işlemler başarısız oldu: {succeeded} başarılı, {failed} başarısız",
|
||||
"renameFailed": "Öğenin adı değiştirilemedi"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Daha küçük:",
|
||||
@@ -713,7 +714,6 @@
|
||||
"disableViewingFilesDescription": "Dosya görüntüleyici/düzenleyicinin devre dışı bırakılacağı dosya uzantılarının boşluk bırakılarak ayrılmış listesi (örn. '.txt .pdf').",
|
||||
"disablePreviewExt": "Belirli dosya uzantıları için ofis dosyası önizlemelerini devre dışı bırakma",
|
||||
"disablePreviewExtDescription": "Ofis dosyası önizlemelerinin devre dışı bırakılacağı dosya uzantılarının boşluk bırakılarak ayrılmış listesi (örn. '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "OnlyOffice düzenleyicisini ve görüntüleyicisini devre dışı bırakmak için dosya uzantılarının boşluk bırakılarak ayrılmış bir listesi. (örneğin, '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Belirli dosya uzantıları için ofis görüntüleyiciyi devre dışı bırakma",
|
||||
"defaultThemeDescription": "default - Varsayılan tema",
|
||||
"customTheme": "Temanızı seçin",
|
||||
@@ -727,7 +727,8 @@
|
||||
"themeAndLanguage": "Tema ve Dil",
|
||||
"previewFolder": "Varsa klasörler için küçük resimleri göster",
|
||||
"previewFolderDescription": "Kapak olarak küçük resmi olan ilk alt öğeye sahip klasörler için küçük resim önizlemelerini gösterin.",
|
||||
"disableFileExtensions": "Devre dışı bırakılacak dosya uzantılarını girin"
|
||||
"disableFileExtensions": "Devre dışı bırakılacak dosya uzantılarını girin",
|
||||
"disableOfficeEditorDescription": "OnlyOffice düzenleyicisinin ve görüntüleyicisinin devre dışı bırakılacağı dosya uzantılarının boşluk bırakılarak ayrılmış bir listesi - veya tümünü devre dışı bırakmak için '*'. (örneğin, '.txt .html .pdf')."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Düzenleyici başlatılamadı. Lütfen yeniden yükleyin.",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "Неможливо скопіювати в буфер обміну",
|
||||
"moveFailed": "Переміщення не вдалося",
|
||||
"operationFailed": "Операція не вдалася",
|
||||
"partialSuccess": "Деякі операції не вдалися: {succeeded} вдалося, {failed} не вдалося"
|
||||
"partialSuccess": "Деякі операції не вдалися: {succeeded} вдалося, {failed} не вдалося",
|
||||
"renameFailed": "Не вдалося перейменувати елемент"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "Менше, ніж:",
|
||||
@@ -580,7 +581,6 @@
|
||||
"disableViewingFilesDescription": "Список розширень файлів через пробіл, для яких слід вимкнути переглядач/редактор файлів (наприклад, '.txt .pdf').",
|
||||
"disablePreviewExt": "Вимкнути попередній перегляд офісних файлів для певних розширень",
|
||||
"disablePreviewExtDescription": "Список розширень файлів через пробіл, для яких слід вимкнути попередній перегляд офісних файлів (наприклад, '.docx .pptx).",
|
||||
"disableOfficeEditorDescription": "Список розділених пробілами розширень файлів, для яких слід вимкнути редактор і переглядач OnlyOffice. (наприклад, '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Вимкнути офісний переглядач для певних розширень файлів",
|
||||
"defaultThemeDescription": "default - тема за замовчуванням",
|
||||
"customTheme": "Виберіть свою тему",
|
||||
@@ -594,7 +594,8 @@
|
||||
"themeAndLanguage": "Тема та мова",
|
||||
"previewFolder": "Показувати мініатюри для папок, якщо вони доступні",
|
||||
"previewFolderDescription": "Показувати попередній перегляд мініатюр для папок з першим дочірнім елементом, що має мініатюру як обкладинку.",
|
||||
"disableFileExtensions": "Введіть розширення файлів для вимкнення"
|
||||
"disableFileExtensions": "Введіть розширення файлів для вимкнення",
|
||||
"disableOfficeEditorDescription": "Список розділених пробілами розширень файлів, для яких слід вимкнути редактор і переглядач OnlyOffice - або '*', щоб вимкнути для всіх. (наприклад, \".txt .html .pdf\")."
|
||||
},
|
||||
"general": {
|
||||
"name": "Ім'я",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "无法复制到剪贴板",
|
||||
"moveFailed": "移动失败",
|
||||
"operationFailed": "操作失败",
|
||||
"partialSuccess": "某些操作失败:{succeeded} 成功,{failed} 失败"
|
||||
"partialSuccess": "某些操作失败:{succeeded} 成功,{failed} 失败",
|
||||
"renameFailed": "重命名项目失败"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "小于",
|
||||
@@ -580,7 +581,6 @@
|
||||
"disableViewingFilesDescription": "以空格分隔的文件扩展名列表,用于禁用文件查看器/编辑器(如\".txt .pdf\")。",
|
||||
"disablePreviewExt": "禁用某些文件扩展名的 Office 文件预览功能",
|
||||
"disablePreviewExtDescription": "用空格分隔的文件扩展名列表,用于禁用办公文件预览(如\".docx .pptx\")。",
|
||||
"disableOfficeEditorDescription": "用空格分隔的文件扩展名列表,用于禁用 OnlyOffice 编辑器和查看器。(例如,\".txt .html .pdf\")。",
|
||||
"disableOfficeEditor": "禁用某些文件扩展名的 Office 查看器",
|
||||
"defaultThemeDescription": "default - 默认主题",
|
||||
"customTheme": "选择主题",
|
||||
@@ -594,7 +594,8 @@
|
||||
"themeAndLanguage": "主题与语言",
|
||||
"previewFolder": "如果有的话,显示文件夹的缩略图",
|
||||
"previewFolderDescription": "显示文件夹的缩略图预览,第一个子项的缩略图为封面。",
|
||||
"disableFileExtensions": "输入要禁用的文件扩展名"
|
||||
"disableFileExtensions": "输入要禁用的文件扩展名",
|
||||
"disableOfficeEditorDescription": "用空格分隔的文件扩展名列表,用于禁用 OnlyOffice 编辑器和查看器,或用 \"*\"表示禁用所有文件扩展名。(例如,\".txt .html .pdf\")。"
|
||||
},
|
||||
"general": {
|
||||
"name": "名称",
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
"copyToClipboardFailed": "無法複製到剪貼簿",
|
||||
"moveFailed": "移動失敗",
|
||||
"operationFailed": "操作失敗",
|
||||
"partialSuccess": "某些作業失敗:{succeeded} 成功,{failed} 失敗"
|
||||
"partialSuccess": "某些作業失敗:{succeeded} 成功,{failed} 失敗",
|
||||
"renameFailed": "重新命名項目失敗"
|
||||
},
|
||||
"search": {
|
||||
"smallerThan": "小於:",
|
||||
@@ -549,7 +550,6 @@
|
||||
"disableViewingFilesDescription": "以空格分隔的檔案副檔名清單,用來停用檔案檢視器/編輯器 (例如:'.txt .pdf')。",
|
||||
"disablePreviewExt": "停用特定檔案副檔名的 Office 檔案預覽",
|
||||
"disablePreviewExtDescription": "以空格分隔的檔案副檔名清單,以停用 Office 檔案預覽 (例如 '.docx .pptx')。",
|
||||
"disableOfficeEditorDescription": "以空格分隔的檔案副檔名清單,用來停用 OnlyOffice 編輯器和檢視器。(例如,'.txt .html .pdf')。",
|
||||
"disableOfficeEditor": "停用特定檔案副檔名的 Office 檢視器",
|
||||
"defaultThemeDescription": "default - 預設主題",
|
||||
"customTheme": "選擇您的主題",
|
||||
@@ -563,7 +563,8 @@
|
||||
"themeAndLanguage": "主題與語言",
|
||||
"previewFolder": "顯示資料夾的縮圖(若有",
|
||||
"previewFolderDescription": "顯示資料夾的縮圖預覽,第一個子項目以縮圖為封面。",
|
||||
"disableFileExtensions": "輸入要停用的檔案副檔名"
|
||||
"disableFileExtensions": "輸入要停用的檔案副檔名",
|
||||
"disableOfficeEditorDescription": "以空格分隔的檔案副檔名清單,用來停用 OnlyOffice 編輯器和檢視器 - 或 '*' 來停用所有。(例如,'.txt .html .pdf')。"
|
||||
},
|
||||
"otp": {
|
||||
"name": "雙因素驗證",
|
||||
|
||||
@@ -433,6 +433,9 @@ export const getters = {
|
||||
officeViewingDisabled: filename => {
|
||||
const ext = ' ' + getFileExtension(filename)
|
||||
const disabledList = state.user.disableOnlyOfficeExt || ''
|
||||
if (disabledList === '*') {
|
||||
return true
|
||||
}
|
||||
if (disabledList !== '') {
|
||||
const disabledExts = ' ' + disabledList.toLowerCase()
|
||||
if (disabledExts.includes(ext.toLowerCase())) {
|
||||
|
||||
+30
-13
@@ -80,6 +80,7 @@ export function removePrefix(path, prefix = "") {
|
||||
|
||||
|
||||
// get path with parameters
|
||||
// Supports array values for repeated parameters (e.g., file=a&file=b&file=c)
|
||||
export function getApiPath(path, params = {}, encode = false) {
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
@@ -89,24 +90,40 @@ export function getApiPath(path, params = {}, encode = false) {
|
||||
const paramKeys = Object.keys(params);
|
||||
if (paramKeys.length > 0) {
|
||||
if (encode) {
|
||||
const encodedParams = paramKeys
|
||||
.filter(key => params[key] !== undefined)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&');
|
||||
if (encodedParams) {
|
||||
path += `?${encodedParams}`;
|
||||
const encodedParams = [];
|
||||
for (const key of paramKeys) {
|
||||
const value = params[key];
|
||||
if (value === undefined) continue;
|
||||
|
||||
// Handle array values for repeated parameters
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
encodedParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
|
||||
});
|
||||
} else {
|
||||
encodedParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
||||
}
|
||||
}
|
||||
if (encodedParams.length > 0) {
|
||||
path += `?${encodedParams.join('&')}`;
|
||||
}
|
||||
} else {
|
||||
path += "?";
|
||||
const queryParams = [];
|
||||
for (const key in params) {
|
||||
if (params[key] === undefined) {
|
||||
continue;
|
||||
const value = params[key];
|
||||
if (value === undefined) continue;
|
||||
|
||||
// Handle array values for repeated parameters
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
queryParams.push(`${key}=${v}`);
|
||||
});
|
||||
} else {
|
||||
queryParams.push(`${key}=${value}`);
|
||||
}
|
||||
path += `${key}=${params[key]}&`;
|
||||
}
|
||||
// remove trailing &
|
||||
if (path.endsWith("&")) {
|
||||
path = path.slice(0, -1);
|
||||
if (queryParams.length > 0) {
|
||||
path += `?${queryParams.join('&')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ export default {
|
||||
mutations.hideTooltip();
|
||||
},
|
||||
validateExtensions(value) {
|
||||
if (value === "") {
|
||||
if (value === "" || value === "*") {
|
||||
return true;
|
||||
}
|
||||
const regex = /^\.\w+(?: \.\w+)*$/;
|
||||
|
||||
@@ -366,7 +366,6 @@ export default {
|
||||
}
|
||||
|
||||
// Checksum feature removed
|
||||
|
||||
const newQueryString = new URLSearchParams(query).toString();
|
||||
const currentQuery = this.$route.query || {};
|
||||
const currentQueryString = new URLSearchParams(
|
||||
|
||||
Reference in new issue
Block a user