Compare commits

...
132 Commits
Author SHA1 Message Date
Graham Steffaniak edaa8c536d updated changelog 2026-01-24 08:35:09 -06:00
Graham Steffaniak 52d57017f2 updated to disable all onlyoffice (#1888) 2026-01-23 14:34:49 -06:00
Graham Steffaniak 1dd219ad18 updated readme 2026-01-23 14:15:18 -06:00
Graham Steffaniak e584c30225 updated readme 2026-01-23 14:13:37 -06:00
Graham Steffaniak 801c029b03 Fix file actions (#1887) 2026-01-23 14:02:33 -06:00
Graham Steffaniak bf46e977c4 updated changelog 2026-01-23 14:02:13 -06:00
Graham Steffaniak c02a994880 fix file actions for scope 2026-01-23 13:51:02 -06:00
Graham Steffaniak 2cf352876c Merge branch 'dev/v1.2.1' of github.com:gtsteffaniak/filebrowser into dev/v1.2.1 2026-01-23 13:29:04 -06:00
Graham Steffaniak fb2c97d05a Db cache size change (#1886) 2026-01-23 13:28:01 -06:00
Graham Steffaniak 0bd58c14ee updated db cache size 2026-01-22 11:30:09 -06:00
Graham Steffaniak 09376139f1 Merge branch 'dev/v1.2.1' of github.com:gtsteffaniak/filebrowser into dev/v1.2.1 2026-01-22 10:52:07 -06:00
Graham Steffaniak 59495b7f2f Db package changes (#1883) 2026-01-22 10:51:56 -06:00
Graham Steffaniak d258333f26 Merge branch 'dev/v1.2.1' of github.com:gtsteffaniak/filebrowser into dev/v1.2.1 2026-01-22 10:28:08 -06:00
Graham Steffaniak fe57dd5edc updated with db name changes 2026-01-22 10:27:46 -06:00
Graham Steffaniak b96636279e updated config and preview (#1878) 2026-01-22 10:26:46 -06:00
Graham Steffaniak 3025442ff8 updated config and preview 2026-01-21 18:18:58 -06:00
Graham Steffaniak 715a044af1 merged 2026-01-21 17:00:39 -06:00
Graham Steffaniak a28d515b90 Merge branch 'dev/v1.2.1' of github.com:gtsteffaniak/filebrowser into dev/v1.2.1 2026-01-21 16:58:59 -06:00
+4 990d53fa95 Upgrade imaging (#1877)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
Co-authored-by: Swedish Wiking <121635232+Swedish-Wiking@users.noreply.github.com>
Co-authored-by: Pyro <pyrodd@gmail.com>
Co-authored-by: lps-rocks <10893911+lps-rocks@users.noreply.github.com>
Co-authored-by: contributor <foss.contributor@gmail.com>
Co-authored-by: Ironic <202444094+YoureIronic@users.noreply.github.com>
Co-authored-by: Sarabanga <96509626+Sarabanga@users.noreply.github.com>
Co-authored-by: ✿ corey <corey@x64.co>
Co-authored-by: Claudio Yanes <me@claudio4.com>
Co-authored-by: Aleksandr Baryshnikov <owner@reddec.net>
Co-authored-by: SmilingPie <86896922+KevinT3Hu@users.noreply.github.com>
Co-authored-by: Maxime Borges <contact@maximeborg.es>
2026-01-21 16:36:14 -06:00
Graham Steffaniak f0068d0765 fix only office integration on 1.2.x (#1873) 2026-01-21 13:27:05 -06:00
Kevin B. 8fd13a40bc Fix listing issues (#1867) 2026-01-21 13:27:05 -06:00
Graham Steffaniak 941841fca1 show new rules style (#1870) 2026-01-21 13:27:05 -06:00
Graham Steffaniak 0235546a8f fix only office integration on 1.2.x (#1873) 2026-01-21 11:41:37 -06:00
Kevin B. 66482cd1d5 Fix listing issues (#1867) 2026-01-21 12:09:45 -05:00
Graham Steffaniak d26189968f show new rules style (#1870) 2026-01-21 10:33:52 -06:00
Graham Steffaniak eb518bac56 updated build for windows (#1859) 2026-01-20 09:24:18 -06:00
Graham Steffaniak d82e7f2986 updated changelog 2026-01-20 08:59:43 -06:00
Graham Steffaniak 7cd3f8374e added last minute bugfixes (#1857) 2026-01-20 08:50:40 -06:00
Graham Steffaniak 4e0f0453f3 updated sharing features (#1852) 2026-01-19 13:48:35 -06:00
Maxime Borges 98246b2981 i18n: Remove unecessary colon for API keys title (#1848) 2026-01-19 10:21:31 -06:00
Maxime Borges d2c07ffd3c settings: do not center settings title (#1849) 2026-01-19 10:21:17 -06:00
Graham SteffaniakandKevin B. 43cbadbeb8 UI improvements shelf (#1843)
Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
2026-01-18 17:26:39 -06:00
Kevin B. 25dc426522 Change to plyr package directly + fixes and improvements (#1833) 2026-01-16 20:47:35 -05:00
Graham Steffaniak 27dbd509b2 Bugfix 1.2.0 (#1840) 2026-01-16 11:22:20 -06:00
Graham Steffaniak 3716c4e4c6 Change quick scan and delete purge (#1835) 2026-01-15 10:34:14 -06:00
Graham Steffaniak 7aabdadf3e Improved api format (#1834) 2026-01-14 18:21:26 -06:00
Graham Steffaniak 264de1ce88 more consistent permissions, functions (#1832) 2026-01-14 11:06:19 -06:00
Graham Steffaniak 4aae24cf11 bulk delete enhancements + duplicate detector deletion feature (#1830) 2026-01-13 11:00:33 -06:00
Graham Steffaniak 0968c44d22 updated scrolling 2026-01-12 14:21:01 -06:00
Graham Steffaniak a15fe4eaa5 Merge branch 'dev/v1.2.0' of github.com:gtsteffaniak/filebrowser into dev/v1.2.0 2026-01-12 12:12:14 -06:00
Graham Steffaniak a433a8a8d4 make disk check and rules more efficient (#1822) 2026-01-12 11:35:25 -06:00
Graham Steffaniak b5219ae6b6 Merge branch 'dev/v1.2.0' of github.com:gtsteffaniak/filebrowser into dev/v1.2.0 2026-01-12 10:46:19 -06:00
Graham SteffaniakandSmilingPie d9f91c7c65 Chunked downloading (#1821)
Co-authored-by: SmilingPie <86896922+KevinT3Hu@users.noreply.github.com>
2026-01-10 11:12:32 -06:00
Kevin B. 4a85746850 Allow rename in previews (#1817) 2026-01-10 11:03:00 -06:00
Graham Steffaniak 3ae8aede25 Merge branch 'dev/v1.2.0' of github.com:gtsteffaniak/filebrowser into dev/v1.2.0 2026-01-09 17:31:33 -06:00
Graham Steffaniak 3990890721 Update login encoding (#1816) 2026-01-09 12:48:26 -06:00
Graham SteffaniakandSmilingPie 726116d831 Search multiple sources (#1815)
Co-authored-by: SmilingPie <86896922+KevinT3Hu@users.noreply.github.com>
2026-01-08 13:35:51 -06:00
Graham Steffaniak 8274f2c715 Merge branch 'dev/v1.2.0' of github.com:gtsteffaniak/filebrowser into dev/v1.2.0 2026-01-08 10:07:48 -06:00
Graham SteffaniakandSmilingPie 32969d0777 Merge v1.1.6 1.2.0 (#1813)
Co-authored-by: SmilingPie <86896922+KevinT3Hu@users.noreply.github.com>
2026-01-07 09:11:56 -06:00
Graham Steffaniak 8f25186136 Merge branch 'main' into dev/v1.2.0 2026-01-07 08:40:48 -06:00
Graham Steffaniak e18a73c97a two pass loading with loading bar (#1800) 2026-01-05 10:45:56 -06:00
Aleksandr Baryshnikov e5cb88ba5d feat: initial support for webdav (#1764) 2026-01-05 09:22:51 -06:00
Kevin B.andGraham Steffaniak 50b9ee8b31 make breadcrumbs act as a drop area (#1785)
Co-authored-by: Graham Steffaniak <gtsteff@gmail.com>
2026-01-04 16:45:38 -06:00
Graham Steffaniak a0e266fadf updated with more indexing changes (#1793) 2026-01-04 13:51:10 -06:00
Graham Steffaniak 377ae33e1b refactor folder size updates (#1782) 2026-01-01 18:36:35 -06:00
Kevin B. e14f2d7a4b Fix fileWatcher file selection + spacebar shortcut (#1770) 2025-12-31 17:16:52 -05:00
Graham Steffaniak fac091f1b3 Merge branch 'main' into dev/v1.2.0 2025-12-31 13:20:37 -06:00
Graham Steffaniak 5330d6b093 updated changelog 2025-12-31 13:19:19 -06:00
Graham Steffaniak 692f99251d updated with a few more small updates (#1768) 2025-12-29 16:09:07 -06:00
Graham Steffaniak 31e70519c1 updated loading spinners (#1766) 2025-12-29 11:05:34 -06:00
Graham Steffaniak 0b88a90048 Merge branch 'main' into dev/v1.2.0 2025-12-28 15:55:05 -06:00
Graham Steffaniak 79d7f07943 updated conditional rules format (#1761) 2025-12-28 15:49:58 -06:00
Graham Steffaniak b562edaaeb make indexing data persistent within cacheDir (#1760) 2025-12-28 11:30:07 -06:00
Graham Steffaniak 33ede042da efficiency tweaks for indexing (#1759) 2025-12-28 09:20:08 -06:00
Graham Steffaniak 036a0ed6c8 updated indexing functionality (#1755) 2025-12-26 16:22:02 -06:00
Graham Steffaniak 1842ff561b updated root scanner not to purge 2025-12-21 08:15:44 -06:00
Graham Steffaniak aed9f7c135 add tail/watch file watcher tool (#1738) 2025-12-20 15:40:29 -06:00
Graham Steffaniak c6600a0665 updated memory pressure (#1741) 2025-12-20 14:10:20 -06:00
Graham Steffaniak 3e3d3decc6 updated indexing design (#1728) 2025-12-16 15:14:50 -06:00
Graham Steffaniak c58b298eb5 Merge branch 'beta/v1.1.4' into dev/v1.2.0 2025-12-13 14:40:49 -06:00
Graham Steffaniak 98ba8095db updated build 2025-12-13 09:54:44 -06:00
Graham Steffaniak 68fb573059 improved cacheDir checks (#1723) 2025-12-13 08:50:05 -06:00
Graham Steffaniak 5747c40dca updated translation 2025-12-12 18:00:04 -06:00
Graham Steffaniak 598bd366db added search image show toggle (#1721) 2025-12-12 17:58:20 -06:00
Graham Steffaniak e3be3300dc File list loading spinner (#1720) 2025-12-12 15:44:27 -06:00
Graham Steffaniak 416aa75446 updated with new logger (#1719) 2025-12-12 15:24:27 -06:00
Graham Steffaniak cfb3b9e869 updated with fix for special characters (#1718) 2025-12-12 14:23:30 -06:00
Graham Steffaniak 166f1bf232 invalidate isDir cache (#1716) 2025-12-12 14:06:41 -06:00
Graham Steffaniak 451f9ac69f update with access rules for shares (#1714) 2025-12-12 12:35:25 -06:00
Graham Steffaniak b2fa1780ed updated with access rule changes (#1713) 2025-12-12 11:27:49 -06:00
Kevin B. 9255bc174b Fix/Improve some behaviors in nextPrevious (#1707) 2025-12-10 16:14:55 -06:00
Kevin B. a3a074d7b6 Fix navigation for files with special characters (#1711) 2025-12-10 14:12:21 -06:00
Claudio YanesandGraham Steffaniak aefe16b508 Implements socket activation (#1698)
Co-authored-by: Graham Steffaniak <42989099+gtsteffaniak@users.noreply.github.com>
2025-12-10 11:54:50 -06:00
Graham Steffaniak 728ee72e2a updated with indexing test fixes (#1699) 2025-12-03 19:33:42 -06:00
Graham Steffaniak e94075de6c updated scanner complexity 2025-12-02 11:08:28 -06:00
Graham Steffaniak c56bafb4e9 fixed some indexing issues 2025-12-02 10:58:37 -06:00
Graham Steffaniak a6e045cc85 Merge tag 'v1.1.3-beta' into experimental-sqlite-indexing 2025-12-02 09:55:22 -06:00
Graham Steffaniak 690685459e updated favicon to svg only (#1694) 2025-12-02 09:09:11 -06:00
Graham Steffaniak 6f6244b2fb revert download behavior (#1691) 2025-12-02 08:54:45 -06:00
Graham Steffaniak 05fcd31909 Clear access cache aggressively (#1690) 2025-12-01 18:28:07 -06:00
Graham Steffaniak a4107a734a continue user dir creation on error (#1689) 2025-12-01 18:06:04 -06:00
Graham Steffaniak 991be79521 scanner size increment changes (#1688) 2025-12-01 17:33:18 -06:00
Graham Steffaniak 8755e82780 improved sqlite implementation 2025-11-30 08:01:14 -06:00
Graham Steffaniak d1b3a045af updated with exclusive lock 2025-11-29 17:51:06 -06:00
Graham Steffaniak b5b365c6a7 Merge branch 'experimental-sqlite-indexing' of github.com:gtsteffaniak/filebrowser into experimental-sqlite-indexing 2025-11-29 17:46:41 -06:00
Graham Steffaniak c33a876033 improved sql indexing 2025-11-29 17:43:20 -06:00
Graham Steffaniak 42778b92bb updated with tuning 2025-11-28 19:59:15 -06:00
Graham Steffaniak ff3b66b79d adjust cache settings 2025-11-28 19:59:15 -06:00
Graham Steffaniak daedeef78d expirmental features 2025-11-28 19:59:15 -06:00
Graham Steffaniak edba1a80ef updated with a few fixes (#1678) 2025-11-28 12:08:17 -06:00
Graham Steffaniak 77fb6bdabc updated with tuning 2025-11-27 14:40:43 -06:00
Graham Steffaniak 97c37caa1c adjust cache settings 2025-11-26 18:37:39 -06:00
Graham Steffaniak 02b28284ca expirmental features 2025-11-26 18:18:12 -06:00
Graham Steffaniak 6a489c6f7c Fix sidebar custom link validation (#1665) 2025-11-26 16:27:29 -06:00
Kevin B.andGraham Steffaniak ba443398ca Improve captions styles in plyr (#1661)
Co-authored-by: Graham Steffaniak <graham.steffaniak@autodesk.com>
2025-11-26 16:06:52 -06:00
Graham Steffaniak 17ff68c00d fix utf-8 filename prefix (#1664) 2025-11-26 16:06:34 -06:00
Graham Steffaniak cb71dd8940 fixed concurrent media duration issue (#1663) 2025-11-26 15:24:12 -06:00
Graham Steffaniak 764155d612 updated indexing changes (#1662) 2025-11-26 14:42:43 -06:00
Graham Steffaniak 9013152d9d Merge branch 'dev/v1.1.2' of github.com:gtsteffaniak/filebrowser into dev/v1.1.2 2025-11-26 09:14:09 -06:00
27bc000cda Experiment with sqllite (#1660)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
Co-authored-by: Swedish Wiking <121635232+Swedish-Wiking@users.noreply.github.com>
Co-authored-by: Pyro <pyrodd@gmail.com>
Co-authored-by: lps-rocks <10893911+lps-rocks@users.noreply.github.com>
Co-authored-by: contributor <foss.contributor@gmail.com>
Co-authored-by: Ironic <202444094+YoureIronic@users.noreply.github.com>
Co-authored-by: Sarabanga <96509626+Sarabanga@users.noreply.github.com>
Co-authored-by: ✿ corey <corey@x64.co>
2025-11-26 09:13:50 -06:00
8f4e420e73 Experiment with sqllite (#1660)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
Co-authored-by: Swedish Wiking <121635232+Swedish-Wiking@users.noreply.github.com>
Co-authored-by: Pyro <pyrodd@gmail.com>
Co-authored-by: lps-rocks <10893911+lps-rocks@users.noreply.github.com>
Co-authored-by: contributor <foss.contributor@gmail.com>
Co-authored-by: Ironic <202444094+YoureIronic@users.noreply.github.com>
Co-authored-by: Sarabanga <96509626+Sarabanga@users.noreply.github.com>
Co-authored-by: ✿ corey <corey@x64.co>
2025-11-26 09:12:09 -06:00
Graham Steffaniak e4d8a5cf20 fix editor issues (#1654) 2025-11-24 13:07:59 -06:00
Kevin B.andGraham Steffaniak fa8ceb50e6 Fix drag and drop and rows in normal view (#1651)
Co-authored-by: Graham Steffaniak <graham.steffaniak@autodesk.com>
2025-11-24 12:12:12 -06:00
Graham Steffaniak c8073db4e6 access control download changes (#1653) 2025-11-24 10:02:39 -06:00
Graham Steffaniak 23fac83e7e adjusted list duration 2025-11-24 09:44:40 -06:00
Graham Steffaniak f36ca31c7f updated some build and comments (#1652) 2025-11-24 09:39:21 -06:00
Graham Steffaniak c0466e96fc updated translation for chinese 2025-11-24 09:05:20 -06:00
Graham Steffaniak dc753fe326 consolidate share url creation to backend api (#1647) 2025-11-24 08:18:32 -06:00
Graham Steffaniak c39a1937b4 updated with various bugfixes (#1646) 2025-11-21 19:30:37 -06:00
Graham Steffaniak 5d38b1e66a Download notification (#1645) 2025-11-21 17:31:31 -06:00
Graham Steffaniak e5e93b4b70 Merge branch 'dev/v1.1.1' of github.com:gtsteffaniak/filebrowser into dev/v1.1.1 2025-11-21 17:10:18 -06:00
eaedbbe05e Fix user notifications (#1644)
Co-authored-by: Swedish Wiking <121635232+Swedish-Wiking@users.noreply.github.com>
Co-authored-by: Pyro <pyrodd@gmail.com>
Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
Co-authored-by: lps-rocks <10893911+lps-rocks@users.noreply.github.com>
Co-authored-by: contributor <foss.contributor@gmail.com>
Co-authored-by: Ironic <202444094+YoureIronic@users.noreply.github.com>
Co-authored-by: Sarabanga <96509626+Sarabanga@users.noreply.github.com>
Co-authored-by: ✿ corey <corey@x64.co>
2025-11-21 16:27:58 -06:00
Graham Steffaniak c6d5f32bd0 Merge branch 'dev/v1.1.1' of github.com:gtsteffaniak/filebrowser into dev/v1.1.1 2025-11-21 16:23:26 -06:00
Graham Steffaniak ee022bc4a9 Fix only office viewing (#1643) 2025-11-21 15:21:54 -06:00
Kevin B. daabefa6c3 Update spanish translation (#1636) 2025-11-21 15:00:28 -06:00
Graham Steffaniak 86aebde566 Merge branch 'dev/v1.1.1' of github.com:gtsteffaniak/filebrowser into dev/v1.1.1 2025-11-21 10:59:58 -06:00
Graham Steffaniak a3b0fc0038 Upgrade ffmpeg 8.0.1 (#1634) 2025-11-20 13:50:57 -06:00
dependabot[bot]andGraham Steffaniak 695364c2df Bump golang.org/x/crypto from 0.44.0 to 0.45.0 in /backend in the go_modules group across 1 directory (#1627)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Graham Steffaniak <42989099+gtsteffaniak@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-20 13:36:46 -06:00
Graham Steffaniak 58a01ff986 Merge branch 'dev/v1.1.1' of github.com:gtsteffaniak/filebrowser into dev/v1.1.1 2025-11-20 13:36:10 -06:00
Graham Steffaniak 3298111560 Add exif tool (#1625) 2025-11-19 17:54:48 -06:00
Graham Steffaniak 3ca9f54de0 updated with compile safe i18n checks (#1623) 2025-11-19 16:48:44 -06:00
Graham Steffaniak 97d16d1663 add exiftool for orientation 2025-11-19 16:46:47 -06:00
85 changed files with 1329 additions and 13066 deletions

No files matched your search

+19
View File
@@ -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.
+4 -2
View File
@@ -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.
+2 -2
View File
@@ -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
View File
@@ -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
}
+31 -35
View File
@@ -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
}
+16 -2
View File
@@ -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 {
+2 -59
View File
@@ -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
}
+4 -3
View File
@@ -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
}
+11 -11
View File
@@ -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
}
+11 -7
View File
@@ -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.
+94 -24
View File
@@ -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
}
+3 -3
View File
@@ -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
}
+9 -9
View File
@@ -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
}
-1
View File
@@ -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
}
-4646
View File
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
+29
View File
@@ -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 != ""
}
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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=
+5 -9
View File
@@ -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
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+13 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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)
}
+22 -16
View File
@@ -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,
+1 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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"
)
+1 -1
View File
@@ -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)
}
+5
View File
@@ -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,
}
+3 -3
View File
@@ -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)
}
+4 -4
View File
@@ -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)
}
-1
View File
@@ -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"
+35
View File
@@ -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)
}
+44 -8
View File
@@ -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
View File
@@ -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
}
+16 -2
View File
@@ -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)
+72
View File
@@ -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
View File
@@ -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) {
+17 -17
View File
@@ -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"
+17 -17
View File
@@ -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"
+24 -17
View File
@@ -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
+16 -1
View File
@@ -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
+3 -4
View File
@@ -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)
+6 -4
View File
@@ -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
}
+6 -3
View File
@@ -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,
+18 -6
View File
@@ -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,
+3
View File
@@ -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() {
+11 -1
View File
@@ -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")
}
});
+48 -7
View File
@@ -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);
}
},
},
+4 -3
View File
@@ -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": "أحمر",
+4 -3
View File
@@ -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",
+4 -3
View File
@@ -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.",
+4 -3
View File
@@ -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": "Απέτυχε η αρχικοποίηση του συντάκτη. Παρακαλούμε επαναφορτώστε.",
+2 -1
View File
@@ -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"
+4 -3
View File
@@ -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",
+4 -3
View File
@@ -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.",
+4 -3
View File
@@ -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": "לא ניתן לאתחל את העורך. אנא טען מחדש.",
+4 -3
View File
@@ -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.",
+4 -3
View File
@@ -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.",
+4 -3
View File
@@ -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": "エディタの初期化に失敗しました。再読み込みしてください。",
+4 -3
View File
@@ -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": "편집기를 초기화하지 못했습니다. 다시 로드하세요.",
+4 -3
View File
@@ -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",
+4 -3
View File
@@ -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.",
+4 -3
View File
@@ -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.",
+4 -3
View File
@@ -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",
+4 -3
View File
@@ -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.",
+4 -3
View File
@@ -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": "Не удалось инициализировать редактор. Пожалуйста, перезагрузитесь.",
+4 -3
View File
@@ -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.",
+4 -3
View File
@@ -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",
+4 -3
View File
@@ -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.",
+4 -3
View File
@@ -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": "Ім'я",
+4 -3
View File
@@ -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": "名称",
+4 -3
View File
@@ -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": "雙因素驗證",
+3
View File
@@ -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
View File
@@ -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('&')}`;
}
}
}
+1 -1
View File
@@ -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(