mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-07-29 18:50:33 +00:00
Compare commits
1205 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
833d908884 | ||
|
|
1cd18d7ead | ||
|
|
e8a388f840 | ||
|
|
0dfe44649c | ||
|
|
05ee853eef | ||
|
|
afeb478ee1 | ||
|
|
df2869993a | ||
|
|
d49ab67904 | ||
|
|
cfff84306a | ||
|
|
72faf6dd3c | ||
|
|
a789f29cee | ||
|
|
74b13db3fa | ||
|
|
773b30102c | ||
|
|
0b925cbca9 | ||
|
|
9dd7c7d099 | ||
|
|
b24e43b06c | ||
|
|
91e68edb8c | ||
|
|
ac11538f08 | ||
|
|
f6fb8f6bf5 | ||
|
|
e6d70cf24c | ||
|
|
41e2b1bbba | ||
|
|
41b01a7404 | ||
|
|
0dd8905886 | ||
|
|
032271bafa | ||
|
|
50125a5d21 | ||
|
|
bc3d75a94a | ||
|
|
fb6aeba9ea | ||
|
|
67e893eee7 | ||
|
|
4a4b0f8c84 | ||
|
|
660d84661d | ||
|
|
392253c563 | ||
|
|
328f629181 | ||
|
|
4c3cb4bc9b | ||
|
|
3213b605be | ||
|
|
610e0b09e9 | ||
|
|
7453c78c98 | ||
|
|
cd5749dff8 | ||
|
|
4daddec6f2 | ||
|
|
9bd79c3aae | ||
|
|
6c69b5cd89 | ||
|
|
4b8a8d72ce | ||
|
|
8ddd3d1db9 | ||
|
|
9fffee387f | ||
|
|
5a12cad548 | ||
|
|
6232686e22 | ||
|
|
b21b1245ae | ||
|
|
7361d91ea2 | ||
|
|
c05c668148 | ||
|
|
b7dc392838 | ||
|
|
ac46cf0671 | ||
|
|
f0785391bf | ||
|
|
9b78324d77 | ||
|
|
fe7efb2e6a | ||
|
|
2651260a1c | ||
|
|
4470288ba1 | ||
|
|
58d22578e8 | ||
|
|
dfc2e887e1 | ||
|
|
aac2516637 | ||
|
|
c05ead7e8e | ||
|
|
d76b7d1610 | ||
|
|
f30fca636c | ||
|
|
ec13054671 | ||
|
|
883a36f02f | ||
|
|
8503ba61ff | ||
|
|
1fb05d65de | ||
|
|
2472fbcd30 | ||
|
|
43a404ca69 | ||
|
|
d9cf2f0100 | ||
|
|
6209f8fddd | ||
|
|
bd1520fe09 | ||
|
|
8cfa6a175f | ||
|
|
a1063925e1 | ||
|
|
64511ce45e | ||
|
|
be23ab3a15 | ||
|
|
ffb486e05f | ||
|
|
403d2bbd33 | ||
|
|
dfe6e5b333 | ||
|
|
d9816b1531 | ||
|
|
7c2c0a11b3 | ||
|
|
3406d3d7f9 | ||
|
|
67ed670d92 | ||
|
|
a1a514dcbb | ||
|
|
5f7311d324 | ||
|
|
998bd95bfa | ||
|
|
c1abe8f561 | ||
|
|
0bb2768754 | ||
|
|
7b7ff8ae8f | ||
|
|
1086903c14 | ||
|
|
3471ec2c4b | ||
|
|
69c76d11cc | ||
|
|
6b04cbf5e9 | ||
|
|
503fd6b01f | ||
|
|
35db07d015 | ||
|
|
19514367ad | ||
|
|
cdd666fc95 | ||
|
|
103acd15fe | ||
|
|
f5e0c4e2e1 | ||
|
|
ca019ae7d9 | ||
|
|
4488f5b131 | ||
|
|
166583db63 | ||
|
|
85b7d2762d | ||
|
|
4edabb9ccc | ||
|
|
103683069e | ||
|
|
5328e80d2e | ||
|
|
847d08bdd1 | ||
|
|
0231b7ebdf | ||
|
|
e07c59df0b | ||
|
|
0d3eb9bea9 | ||
|
|
34ae34e764 | ||
|
|
ca0108f070 | ||
|
|
a1e442ef9e | ||
|
|
6ad8160aa3 | ||
|
|
22b848f26e | ||
|
|
a418dd6bb3 | ||
|
|
d978d1ecca | ||
|
|
9cc18a81e3 | ||
|
|
e38c28273a | ||
|
|
da2dff0933 | ||
|
|
261bca67bc | ||
|
|
ca5e249e3c | ||
|
|
f4e148523e | ||
|
|
1f22fe65ec | ||
|
|
e2bdf6f747 | ||
|
|
d236f1c563 | ||
|
|
4edf425a9f | ||
|
|
a1a7ac4f04 | ||
|
|
74917c8037 | ||
|
|
7bd27f5e82 | ||
|
|
9f4288b1b4 | ||
|
|
41b801d30c | ||
|
|
da6b7ac26b | ||
|
|
7f8b920aa4 | ||
|
|
dd53644acb | ||
|
|
9b80a9aa6c | ||
|
|
0321415a15 | ||
|
|
23e84c9974 | ||
|
|
0fadf28b18 | ||
|
|
871f337892 | ||
|
|
29c73eaca6 | ||
|
|
f13c7c8cff | ||
|
|
1e03feadb5 | ||
|
|
8adf127c7d | ||
|
|
7dbf7a3528 | ||
|
|
65a837de49 | ||
|
|
876cdb3426 | ||
|
|
2f805de527 | ||
|
|
7a16129bfc | ||
|
|
0f39bd055e | ||
|
|
860c19ddf5 | ||
|
|
a8fc1657b7 | ||
|
|
c406bda0c7 | ||
|
|
d9f9460c1e | ||
|
|
126227bb27 | ||
|
|
b6a4fb1f27 | ||
|
|
2368e468b9 | ||
|
|
8d8cd26d7f | ||
|
|
8f81b77cf2 | ||
|
|
0616f6811f | ||
|
|
432f3e60ff | ||
|
|
ae72f93667 | ||
|
|
4812536555 | ||
|
|
6d44b3ae1d | ||
|
|
811cf2dfe5 | ||
|
|
b5f970731b | ||
|
|
fc80f4f44c | ||
|
|
6aea227617 | ||
|
|
09a26166b4 | ||
|
|
a63573b67e | ||
|
|
4bd7d69c82 | ||
|
|
c21af0791a | ||
|
|
858eb42651 | ||
|
|
21709428b1 | ||
|
|
846fb3333a | ||
|
|
6f772f2b10 | ||
|
|
3c5d36673f | ||
|
|
27afbb8813 | ||
|
|
79875bac7f | ||
|
|
be8ba18937 | ||
|
|
ef2e9992dc | ||
|
|
0542fc0ba4 | ||
|
|
f04af0cac6 | ||
|
|
f5f8b60b33 | ||
|
|
6dcef07f40 | ||
|
|
6700a981a5 | ||
|
|
490e5bbf99 | ||
|
|
df63cb595c | ||
|
|
3cb21c727d | ||
|
|
c01b6a840c | ||
|
|
cbcf708d94 | ||
|
|
4d9e6b8218 | ||
|
|
8ee55761a1 | ||
|
|
8598db2acc | ||
|
|
a7dc7bf88a | ||
|
|
9f56826fb2 | ||
|
|
d7b00ce5f6 | ||
|
|
177c7cfcce | ||
|
|
4af3f85e64 | ||
|
|
c950a57df8 | ||
|
|
7ed1425115 | ||
|
|
148b3c5942 | ||
|
|
a40f80672d | ||
|
|
8dc618a24b | ||
|
|
31194fb57a | ||
|
|
aa809096eb | ||
|
|
e3d00d591b | ||
|
|
7b16e2de90 | ||
|
|
2470b9eb69 | ||
|
|
9940bdd663 | ||
|
|
200d501547 | ||
|
|
3169a14a4d | ||
|
|
7e78ad97ad | ||
|
|
0467326d5c | ||
|
|
63a76ef18c | ||
|
|
3cad9a2b0e | ||
|
|
804b14b698 | ||
|
|
95e6ed75a7 | ||
|
|
b09960e538 | ||
|
|
5e8f5be245 | ||
|
|
e5bc0d3cce | ||
|
|
f67bccf8c5 | ||
|
|
88b97def9e | ||
|
|
1f7904dad2 | ||
|
|
5feaf66575 | ||
|
|
c94870fcfe | ||
|
|
6a76dfeba9 | ||
|
|
e193d43278 | ||
|
|
489af403a1 | ||
|
|
ff2f00498c | ||
|
|
2957b4605b | ||
|
|
099dfb0ae8 | ||
|
|
a4289011ef | ||
|
|
942d59848a | ||
|
|
854e5371b6 | ||
|
|
b9df030e6e | ||
|
|
2b82612e3f | ||
|
|
08d7a1504c | ||
|
|
b8da36e630 | ||
|
|
a8fe32f4aa | ||
|
|
8fee2561af | ||
|
|
d441b28f43 | ||
|
|
87cfead033 | ||
|
|
3a08949c7f | ||
|
|
b594d4d4e2 | ||
|
|
1053aace8c | ||
|
|
004488c15b | ||
|
|
3032a1fade | ||
|
|
87f1d00fd7 | ||
|
|
f0f2f1ff06 | ||
|
|
08c8ede587 | ||
|
|
1154b49974 | ||
|
|
785b7abb7b | ||
|
|
550a73b6ba | ||
|
|
e7ea1ad27d | ||
|
|
24781badd4 | ||
|
|
4094fb359b | ||
|
|
cfa6c5864e | ||
|
|
59ca0c340a | ||
|
|
037e29a708 | ||
|
|
fda8a99292 | ||
|
|
208535a8cc | ||
|
|
464b581953 | ||
|
|
20312ff18e | ||
|
|
219582c0b0 | ||
|
|
4fd18a382c | ||
|
|
94ec786d34 | ||
|
|
25e9b85259 | ||
|
|
ffa893e9ac | ||
|
|
b8151a038a | ||
|
|
943e5340d0 | ||
|
|
88aa56f53b | ||
|
|
a12a612970 | ||
|
|
a2d80c62c1 | ||
|
|
1faa21844f | ||
|
|
7a93b2a062 | ||
|
|
032d6c7520 | ||
|
|
9595f3939c | ||
|
|
60b1ee8bb9 | ||
|
|
79d1aa9229 | ||
|
|
20bfd131c6 | ||
|
|
f89975603e | ||
|
|
2d3b64ecf6 | ||
|
|
579756dbb5 | ||
|
|
c11c986b73 | ||
|
|
3fdca6dfd9 | ||
|
|
cf966578d8 | ||
|
|
6d4c867672 | ||
|
|
169e97e6f9 | ||
|
|
7fa3432f25 | ||
|
|
e9ce7fa5aa | ||
|
|
849f5ad443 | ||
|
|
c1715992bd | ||
|
|
e4f2503298 | ||
|
|
152f8302f7 | ||
|
|
4cbb4b73af | ||
|
|
58cc874828 | ||
|
|
124def5cd7 | ||
|
|
2d88c06761 | ||
|
|
204a3f0eea | ||
|
|
f029c3005e | ||
|
|
a6934e40ff | ||
|
|
98662ac5ec | ||
|
|
5cf8ce8db5 | ||
|
|
062dc414f8 | ||
|
|
63582b644c | ||
|
|
4302ece49b | ||
|
|
e1ee14d827 | ||
|
|
84ca722261 | ||
|
|
b9ac45d5da | ||
|
|
701522a060 | ||
|
|
78e0395960 | ||
|
|
f0680cf0f5 | ||
|
|
982405ec94 | ||
|
|
a78aaed214 | ||
|
|
df11a7dd0e | ||
|
|
79980bcf52 | ||
|
|
3be134f23d | ||
|
|
279a5ccd1e | ||
|
|
87f73ac982 | ||
|
|
85cde140ba | ||
|
|
119609c834 | ||
|
|
d48f5665d6 | ||
|
|
54306bdc87 | ||
|
|
33deedf559 | ||
|
|
88d1eecc4e | ||
|
|
43db19f8c8 | ||
|
|
a360f26979 | ||
|
|
ab367a2740 | ||
|
|
5df5508a85 | ||
|
|
6d5aa355e4 | ||
|
|
a3b5584505 | ||
|
|
8db2411cd4 | ||
|
|
c284de9d2c | ||
|
|
984ea7b569 | ||
|
|
fd7b70cf38 | ||
|
|
13e3b46718 | ||
|
|
d759ab0bd8 | ||
|
|
00323a8f37 | ||
|
|
420adea7e6 | ||
|
|
f576d38a7e | ||
|
|
9bdc67c207 | ||
|
|
f41585f039 | ||
|
|
89be0b1873 | ||
|
|
8c5dc7641e | ||
|
|
0a0cb8046f | ||
|
|
f89435c068 | ||
|
|
fb8d41eb9a | ||
|
|
0fadaccaa2 | ||
|
|
e24e1f1aba | ||
|
|
5de4099cba | ||
|
|
d01493106d | ||
|
|
2d9689dd6a | ||
|
|
c4c1cea230 | ||
|
|
ceb5e723f3 | ||
|
|
ebc7d2303d | ||
|
|
23c4e4565b | ||
|
|
17f1e08a58 | ||
|
|
ffc850454e | ||
|
|
13814e1119 | ||
|
|
4e9e312984 | ||
|
|
ce3b407c51 | ||
|
|
fb5d099f85 | ||
|
|
1ace579a55 | ||
|
|
ac7b49c148 | ||
|
|
9d44932dba | ||
|
|
0d973d3aad | ||
|
|
cacc0999e9 | ||
|
|
42d1b6f3ae | ||
|
|
bb10c3dfa9 | ||
|
|
ce76aa23a6 | ||
|
|
94b635daf8 | ||
|
|
31871aaa4b | ||
|
|
9d465663db | ||
|
|
70081f2647 | ||
|
|
fa9d2f266f | ||
|
|
8fcfb502ca | ||
|
|
0bab2aba9e | ||
|
|
38951d950f | ||
|
|
dda8fdbcb2 | ||
|
|
bf3ba65782 | ||
|
|
f35b7c9d9d | ||
|
|
e9506c3eae | ||
|
|
bb4465548b | ||
|
|
cf8b5ca768 | ||
|
|
f93d760b1b | ||
|
|
1495ee8dd8 | ||
|
|
8a7279e3ee | ||
|
|
fdff7a38f4 | ||
|
|
f26a68587d | ||
|
|
1c62038344 | ||
|
|
6eb7b4b8ec | ||
|
|
f11fc37409 | ||
|
|
d12a3dc8a8 | ||
|
|
0cfab8770a | ||
|
|
3876ae8fe8 | ||
|
|
77644e4425 | ||
|
|
6592782dc0 | ||
|
|
d6dc250ed4 | ||
|
|
9579f14c34 | ||
|
|
c5acbffe3f | ||
|
|
63142042bc | ||
|
|
1ac0305ed0 | ||
|
|
7860013aa9 | ||
|
|
7a5b964611 | ||
|
|
6950c2e4d2 | ||
|
|
291223b3ce | ||
|
|
99aeb766c3 | ||
|
|
93fe31cc55 | ||
|
|
b9a03fabd9 | ||
|
|
d00b3ea8f8 | ||
|
|
c18afcddc4 | ||
|
|
57db25d08a | ||
|
|
b8f64a1c1b | ||
|
|
de35dee1c5 | ||
|
|
dd883985bb | ||
|
|
97b8911ba8 | ||
|
|
a397e7305d | ||
|
|
d0039afbb7 | ||
|
|
878cdfbc52 | ||
|
|
1165f00bd4 | ||
|
|
949ddffef2 | ||
|
|
c4725428e0 | ||
|
|
d29ad356d1 | ||
|
|
692ca5eaf0 | ||
|
|
b9787c78f3 | ||
|
|
dec7a02737 | ||
|
|
0eade717ce | ||
|
|
e6c674b3c6 | ||
|
|
4ff247e134 | ||
|
|
2f0c1f5fa2 | ||
|
|
07692653ff | ||
|
|
82dc57ad43 | ||
|
|
84e8632b98 | ||
|
|
571ce6cb0d | ||
|
|
783503aece | ||
|
|
b482a9bf0d | ||
|
|
36c6cc203e | ||
|
|
8950585141 | ||
|
|
950028abeb | ||
|
|
280fa562a6 | ||
|
|
6b1fa87ad3 | ||
|
|
cacfb2bc08 | ||
|
|
3107ae4147 | ||
|
|
c182114883 | ||
|
|
342b239ac6 | ||
|
|
0f41aac20b | ||
|
|
cd51a59e72 | ||
|
|
c829330b53 | ||
|
|
c14cf86f83 | ||
|
|
6d620c00a1 | ||
|
|
06e8713fa5 | ||
|
|
af9b42549f | ||
|
|
75baf7ce33 | ||
|
|
4ff6347155 | ||
|
|
14ee054359 | ||
|
|
7f559ffd07 | ||
|
|
619f6837b0 | ||
|
|
d778c192ae | ||
|
|
a290c6d7db | ||
|
|
c1b0207800 | ||
|
|
c7a5c7efee | ||
|
|
cbeec6d225 | ||
|
|
25e47c3ce8 | ||
|
|
5eb3bf4058 | ||
|
|
07dfdce8e4 | ||
|
|
e5e1b6dee4 | ||
|
|
1582b8b2cd | ||
|
|
21ad653b7e | ||
|
|
5b7ea9f95a | ||
|
|
607f5708a2 | ||
|
|
d61110e4d7 | ||
|
|
7e758357d1 | ||
|
|
3faec03ed7 | ||
|
|
a7a68f74ae | ||
|
|
6425cc58b4 | ||
|
|
88f1442932 | ||
|
|
545c972214 | ||
|
|
124abc7643 | ||
|
|
b8454bb2e4 | ||
|
|
035084d8e8 | ||
|
|
9072cbce34 | ||
|
|
e6ffb65374 | ||
|
|
5c5942d995 | ||
|
|
1a5c83bcfe | ||
|
|
5a8e7171b1 | ||
|
|
0f27c91eca | ||
|
|
7c716862c1 | ||
|
|
01c814cf98 | ||
|
|
35ca24adb8 | ||
|
|
14b0dfec34 | ||
|
|
528ce92fad | ||
|
|
fbe169b84f | ||
|
|
b4eddf45e4 | ||
|
|
0614dcd89b | ||
|
|
fcb248a5fe | ||
|
|
bf73e4dea3 | ||
|
|
b28952cb25 | ||
|
|
1e96fd9035 | ||
|
|
e423395ef0 | ||
|
|
65bbf44e3c | ||
|
|
200b9a6c26 | ||
|
|
3645b578cd | ||
|
|
cc6db83988 | ||
|
|
046d6193c5 | ||
|
|
244fda2f2c | ||
|
|
e36a9b40a0 | ||
|
|
a756e02142 | ||
|
|
b6394745a3 | ||
|
|
e99e0b3028 | ||
|
|
47b3e218ad | ||
|
|
0c34b79a99 | ||
|
|
04166e81e5 | ||
|
|
fae410ce6e | ||
|
|
9da01be7fc | ||
|
|
e9e7c68557 | ||
|
|
8ef8f2c098 | ||
|
|
3b3df83d64 | ||
|
|
38d0366acf | ||
|
|
4403cd3572 | ||
|
|
8d7522049c | ||
|
|
7b43cfb1dc | ||
|
|
d644744417 | ||
|
|
d1a73a8b18 | ||
|
|
2b5d6cbb99 | ||
|
|
364f391017 | ||
|
|
c13861e13c | ||
|
|
e6b750add5 | ||
|
|
70d59ec03e | ||
|
|
bf37f88c32 | ||
|
|
7354eb6cf9 | ||
|
|
10684e5390 | ||
|
|
58fe817349 | ||
|
|
d8472e767b | ||
|
|
8700cb30ff | ||
|
|
93c4b2e03c | ||
|
|
2d1a82b73f | ||
|
|
5a07291306 | ||
|
|
09f679fae4 | ||
|
|
9e273cd947 | ||
|
|
77d266bc00 | ||
|
|
8861933cf8 | ||
|
|
a5ea2a266b | ||
|
|
f5e531c8ae | ||
|
|
6072540c3e | ||
|
|
464b644adf | ||
|
|
089255997a | ||
|
|
5331969163 | ||
|
|
f32f27383d | ||
|
|
0acd69c537 | ||
|
|
ae4fb0ea25 | ||
|
|
8230eb7ab5 | ||
|
|
8b8fb3343f | ||
|
|
1d494ff315 | ||
|
|
da03728cd7 | ||
|
|
e735491c57 | ||
|
|
4d830f707f | ||
|
|
f84a6db680 | ||
|
|
a430eb2e60 | ||
|
|
c232d41f90 | ||
|
|
c1e4fd648b | ||
|
|
d5b39a14fd | ||
|
|
e2e1e49130 | ||
|
|
b0f92dd2d7 | ||
|
|
21b0827808 | ||
|
|
d6d84e2b48 | ||
|
|
ca86f91621 | ||
|
|
4bfbf33249 | ||
|
|
f19943a42e | ||
|
|
e74c958862 | ||
|
|
221451a517 | ||
|
|
f46641b038 | ||
|
|
23bd8f6715 | ||
|
|
506fc08577 | ||
|
|
f33076462a | ||
|
|
6c29fabdc8 | ||
|
|
0268506f80 | ||
|
|
ad864a97e9 | ||
|
|
f714e71a35 | ||
|
|
dbdbbab4d7 | ||
|
|
7c0c7820ef | ||
|
|
2741616473 | ||
|
|
ffb858e4ef | ||
|
|
0ca8059d8d | ||
|
|
8ca080422f | ||
|
|
cbb712484d | ||
|
|
8a14018861 | ||
|
|
a493ec90ff | ||
|
|
33113036cd | ||
|
|
a02b2972eb | ||
|
|
e9bb3dc243 | ||
|
|
2e26393a02 | ||
|
|
04a13f086f | ||
|
|
1cc539eb8a | ||
|
|
6ebfdcceaa | ||
|
|
db671c227b | ||
|
|
4aee14de44 | ||
|
|
0cca7d8dc0 | ||
|
|
c34c0afecf | ||
|
|
56a0f9244b | ||
|
|
56b80b6d9b | ||
|
|
d9ebd65ffc | ||
|
|
a882fb6c85 | ||
|
|
5daae69a6d | ||
|
|
54a1ae0fa0 | ||
|
|
b6b4fb5da7 | ||
|
|
6d82a27f9a | ||
|
|
31a326606d | ||
|
|
abbf203bdd | ||
|
|
e82e2392a4 | ||
|
|
f4a8420bf3 | ||
|
|
71a8f5662c | ||
|
|
48f894740f | ||
|
|
b883e287a0 | ||
|
|
5d9f0977d6 | ||
|
|
c606a01a2d | ||
|
|
54b91b8ff0 | ||
|
|
a46acba5f9 | ||
|
|
1d14798653 | ||
|
|
6d55cc59f7 | ||
|
|
495e731ee7 | ||
|
|
ff1579b950 | ||
|
|
7a48fd0c3e | ||
|
|
cfea84fd5e | ||
|
|
0ba9505a19 | ||
|
|
5355629fd1 | ||
|
|
f8a16a6aca | ||
|
|
35d1c09243 | ||
|
|
99c64c12ed | ||
|
|
3d6c5152fe | ||
|
|
ba797cda31 | ||
|
|
5300d00d2e | ||
|
|
bbdd313705 | ||
|
|
045064f8b8 | ||
|
|
252f0a7533 | ||
|
|
1194cfe009 | ||
|
|
0201f9c5c4 | ||
|
|
cc331383fb | ||
|
|
d1c84a8412 | ||
|
|
e92dbb4bb8 | ||
|
|
209acf2429 | ||
|
|
25372edb5c | ||
|
|
d51a343820 | ||
|
|
065959451d | ||
|
|
2fdea73430 | ||
|
|
129a4fd39d | ||
|
|
64400ffda8 | ||
|
|
03d74ee758 | ||
|
|
2b37e696c9 | ||
|
|
21d5ee1b97 | ||
|
|
ec7b643e8e | ||
|
|
d729701bd4 | ||
|
|
406d4f7884 | ||
|
|
1e7c41505f | ||
|
|
bb5d192095 | ||
|
|
121d9abecd | ||
|
|
7de6bc4a91 | ||
|
|
2369e5c0ed | ||
|
|
056cfa8fac | ||
|
|
e7d77106ab | ||
|
|
a6347c8858 | ||
|
|
b596567c61 | ||
|
|
364fdaaf0c | ||
|
|
8b75aefb1c | ||
|
|
b0f4604f44 | ||
|
|
f6f7e5fea3 | ||
|
|
043cdbf402 | ||
|
|
8e67a12f26 | ||
|
|
83898d616f | ||
|
|
090272e3b7 | ||
|
|
10bf3cffbf | ||
|
|
99a6382b32 | ||
|
|
a53aac1c30 | ||
|
|
21783ed91a | ||
|
|
7be5644952 | ||
|
|
30a8ddf113 | ||
|
|
c3465f9913 | ||
|
|
e8589be640 | ||
|
|
eb3978ea55 | ||
|
|
d6cdf0e435 | ||
|
|
1fccc5d649 | ||
|
|
a8388689f3 | ||
|
|
2a90cdfdaf | ||
|
|
6ca3d5a573 | ||
|
|
3b48f75301 | ||
|
|
4c5b612cb2 | ||
|
|
e336a25ad2 | ||
|
|
c9e05f98c4 | ||
|
|
be62f56782 | ||
|
|
2e47a038d6 | ||
|
|
a9c327cc06 | ||
|
|
782375b1cb | ||
|
|
5d5e8ed422 | ||
|
|
5f57cf9e41 | ||
|
|
4786187852 | ||
|
|
236ca637f9 | ||
|
|
e2d72706cc | ||
|
|
da5a6e051f | ||
|
|
bee71d93fe | ||
|
|
821f51ea5a | ||
|
|
434e49bf59 | ||
|
|
61f25086c3 | ||
|
|
18f04a7d26 | ||
|
|
22a05e1f02 | ||
|
|
b4b4b0efc9 | ||
|
|
8fd6c55a0e | ||
|
|
a9da7fd56c | ||
|
|
6b77b8d683 | ||
|
|
e39ea73095 | ||
|
|
0e0b0c8095 | ||
|
|
ae0af1f996 | ||
|
|
d194d71293 | ||
|
|
bbd0abbdfd | ||
|
|
5100e587d7 | ||
|
|
0e3b35b30e | ||
|
|
05bfae264a | ||
|
|
4c233c3db3 | ||
|
|
7797a4ef18 | ||
|
|
d70650689c | ||
|
|
8dddc8a450 | ||
|
|
cdf8def330 | ||
|
|
e167c3e1ef | ||
|
|
fe5ca74aa1 | ||
|
|
dfad87386f | ||
|
|
6d7ba65faf | ||
|
|
d5487ba6fa | ||
|
|
34a08170c8 | ||
|
|
d49c3dfacf | ||
|
|
2cfee2183c | ||
|
|
fb1a09c7c1 | ||
|
|
b19710efca | ||
|
|
ff9502ff34 | ||
|
|
62f0dfb302 | ||
|
|
81cd8fc6d3 | ||
|
|
70c826133b | ||
|
|
2f6c473977 | ||
|
|
bf36cc00f1 | ||
|
|
883383a571 | ||
|
|
0a05f8c01f | ||
|
|
a4b089a6db | ||
|
|
5c5ab6b875 | ||
|
|
04e03a83b4 | ||
|
|
c4e955acf4 | ||
|
|
fc04578e28 | ||
|
|
3264cea830 | ||
|
|
748af7172c | ||
|
|
da595326ee | ||
|
|
821fba41a2 | ||
|
|
cfafefa35a | ||
|
|
391a078cd4 | ||
|
|
fc2ee37353 | ||
|
|
a09dfa8d9f | ||
|
|
4dbc802972 | ||
|
|
d59ad594b8 | ||
|
|
a4cb813ddf | ||
|
|
4d0a68e787 | ||
|
|
a744bd224f | ||
|
|
da1fe7c9d7 | ||
|
|
7fabadc871 | ||
|
|
c3079d30e2 | ||
|
|
6a31af6c0a | ||
|
|
21d361ad30 | ||
|
|
d574fb6d1a | ||
|
|
bb4bb508a9 | ||
|
|
edd808f124 | ||
|
|
cdcd9a313a | ||
|
|
d0c3aeace1 | ||
|
|
9484454584 | ||
|
|
bd3c1941ff | ||
|
|
01f7842a18 | ||
|
|
38f7788255 | ||
|
|
c1fb4004f7 | ||
|
|
584b706b1e | ||
|
|
ecdd684bf1 | ||
|
|
36af01daa6 | ||
|
|
d0c3b8033d | ||
|
|
aa00c1c89c | ||
|
|
a404fb043d | ||
|
|
95fec7f694 | ||
|
|
5994224468 | ||
|
|
374bbd3ec1 | ||
|
|
2c97573301 | ||
|
|
70eba7ecc9 | ||
|
|
7a4d0c0c08 | ||
|
|
8838a09cf5 | ||
|
|
184b7c14f2 | ||
|
|
289c8e6f32 | ||
|
|
ff1e0b8185 | ||
|
|
0ac39684f1 | ||
|
|
fa390c498d | ||
|
|
4b72bbfc7f | ||
|
|
2a4a46c61a | ||
|
|
efd41cc4c1 | ||
|
|
912f27a9e3 | ||
|
|
4e28cc13c9 | ||
|
|
f37513c45e | ||
|
|
4d77ce0955 | ||
|
|
66dfbb303c | ||
|
|
9bf6b856e5 | ||
|
|
051104bfa0 | ||
|
|
b8ee3404ee | ||
|
|
853ec906ef | ||
|
|
7b35815754 | ||
|
|
2744f7d5b9 | ||
|
|
b508ac3d4f | ||
|
|
ff4375cf6c | ||
|
|
a664ba1f9d | ||
|
|
bb3486286c | ||
|
|
ecfcbfd216 | ||
|
|
9bcfa900f9 | ||
|
|
c2f1423c02 | ||
|
|
6744cd47ce | ||
|
|
a4ef02a47b | ||
|
|
1a5b999545 | ||
|
|
10d628aecc | ||
|
|
fa95299df4 | ||
|
|
fd22e0b163 | ||
|
|
428c1c606d | ||
|
|
60d1e2d291 | ||
|
|
11e9202160 | ||
|
|
59619ba34f | ||
|
|
73dd066670 | ||
|
|
2b2c1085fb | ||
|
|
02db83c72e | ||
|
|
3a0dace9a9 | ||
|
|
a5757b94e8 | ||
|
|
1ebfc64ea1 | ||
|
|
2c14146a31 | ||
|
|
a49105db1d | ||
|
|
0401adf7f4 | ||
|
|
c1e6d5869a | ||
|
|
db0a23aec0 | ||
|
|
350c73d78e | ||
|
|
daf36b28fd | ||
|
|
57c99e0e26 | ||
|
|
aaed985699 | ||
|
|
0ed32c6af8 | ||
|
|
dda9a389f3 | ||
|
|
f80b016ef0 | ||
|
|
ceec4dcfe6 | ||
|
|
7177184678 | ||
|
|
0523b31b96 | ||
|
|
80030dee32 | ||
|
|
cb43770025 | ||
|
|
eaba7e5255 | ||
|
|
49dbacdccd | ||
|
|
d94acdd89a | ||
|
|
06d9c03e92 | ||
|
|
9d54046140 | ||
|
|
dec3d629d4 | ||
|
|
8118afd0ac | ||
|
|
577c0efa9c | ||
|
|
dcf0bc65bf | ||
|
|
c211b96719 | ||
|
|
1e7d3b25c2 | ||
|
|
b16982df0f | ||
|
|
540ddf47a7 | ||
|
|
02730bb9bf | ||
|
|
d1d8e3e340 | ||
|
|
42a39b3f1d | ||
|
|
dd503695a1 | ||
|
|
1d66bbe40a | ||
|
|
5da9d74da6 | ||
|
|
b14b9114f8 | ||
|
|
8a43413f88 | ||
|
|
c3bd1188aa | ||
|
|
fc209f64de | ||
|
|
96afaca0ad | ||
|
|
f663237a16 | ||
|
|
ac3ead8dce | ||
|
|
7c9a75e725 | ||
|
|
596c73288f | ||
|
|
d1d7b23da6 | ||
|
|
e677c78471 | ||
|
|
9734f707f0 | ||
|
|
e5fa96b666 | ||
|
|
bcef7d3f73 | ||
|
|
aed3af5838 | ||
|
|
6bd34c7632 | ||
|
|
040584c865 | ||
|
|
ecb2d1d81b | ||
|
|
a74c72db45 | ||
|
|
f5b1e10618 | ||
|
|
e7fed5a45b | ||
|
|
f8dfbf7eee | ||
|
|
fca5fc5b87 | ||
|
|
4ee19be63d | ||
|
|
b2ad3f7368 | ||
|
|
b73d278ded | ||
|
|
6366cf0b18 | ||
|
|
f73518029c | ||
|
|
c782f21b0f | ||
|
|
0942fc7042 | ||
|
|
c1987237d0 | ||
|
|
cf85404dd2 | ||
|
|
6f226fa549 | ||
|
|
228ebea66c | ||
|
|
bb19834042 | ||
|
|
7870e89bc0 | ||
|
|
8888b9f446 | ||
|
|
f6e5c6f0de | ||
|
|
e7659ea36b | ||
|
|
7730ccd611 | ||
|
|
80890075e8 | ||
|
|
9b04004120 | ||
|
|
a73d7f14b7 | ||
|
|
ffe960a8c2 | ||
|
|
73c80732d9 | ||
|
|
8e2663bf7b | ||
|
|
e697e58164 | ||
|
|
c01496624a | ||
|
|
8906408a8f | ||
|
|
3ec7951380 | ||
|
|
b30aefa522 | ||
|
|
bc8a750dfe | ||
|
|
f1f7f17ade | ||
|
|
9182d33e1c | ||
|
|
7d836a3728 | ||
|
|
010d16fc1d | ||
|
|
fa89ba4665 | ||
|
|
a0752904c1 | ||
|
|
371718634b | ||
|
|
0f4f8751f2 | ||
|
|
ec45ee471f | ||
|
|
6fffcbac4e | ||
|
|
2948589fcd | ||
|
|
ecd0b2ee0d | ||
|
|
205f11d677 | ||
|
|
949f0f277f | ||
|
|
665e45889c | ||
|
|
8d87e0d5f9 | ||
|
|
46d80464d2 | ||
|
|
829ed9fb6d | ||
|
|
988d3e5bdd | ||
|
|
6eb3ab0635 | ||
|
|
c2e03bbfab | ||
|
|
608a0015ee | ||
|
|
f81857acce | ||
|
|
b1e0d5b39f | ||
|
|
68cf7a2173 | ||
|
|
89d1c06441 | ||
|
|
2bebb5f0f8 | ||
|
|
683b11d265 | ||
|
|
4d1b9dd211 | ||
|
|
b8f35ce932 | ||
|
|
a078f0b787 | ||
|
|
7401d16e45 | ||
|
|
958a44f95e | ||
|
|
e08239781f | ||
|
|
c29698dffa | ||
|
|
81de95632a | ||
|
|
7f2d221083 | ||
|
|
74b7cd8e81 | ||
|
|
6cb51b4eb4 | ||
|
|
f09bf3e1d0 | ||
|
|
6f345be3e4 | ||
|
|
ddd4ffa4ca | ||
|
|
deabc80fd7 | ||
|
|
b6a51bed51 | ||
|
|
0426629a59 | ||
|
|
0358e42d2c | ||
|
|
3768e3345f | ||
|
|
16e434be66 | ||
|
|
bf303c536a | ||
|
|
43a460993c | ||
|
|
7f0673ee70 | ||
|
|
4c3099a086 | ||
|
|
f0bc9167b1 | ||
|
|
23d646c456 | ||
|
|
76add9e527 | ||
|
|
c63cc5a2d2 | ||
|
|
25c8788390 | ||
|
|
aa52b07bb1 | ||
|
|
76b466f649 | ||
|
|
8ecc2da947 | ||
|
|
8650d2ffe7 | ||
|
|
34d7d2c8c4 | ||
|
|
201329abce | ||
|
|
f2b5dd3787 | ||
|
|
5072bbb2cb | ||
|
|
6b19ab6613 | ||
|
|
730be5ef6b | ||
|
|
46ee595389 | ||
|
|
dee465ab86 | ||
|
|
209f9fa77f | ||
|
|
ba8c09f454 | ||
|
|
16a34defc0 | ||
|
|
7d1e03075d | ||
|
|
1c25f6ee69 | ||
|
|
aa172b8bb5 | ||
|
|
4711e7bcd5 | ||
|
|
8a47aee137 | ||
|
|
190cb99a79 | ||
|
|
603203848a | ||
|
|
5e6f14b5dc | ||
|
|
976eb5583d | ||
|
|
b92152693f | ||
|
|
7ec24d9d77 | ||
|
|
20ebbf6611 | ||
|
|
ba7e71a7c3 | ||
|
|
8973c4598f | ||
|
|
18889ad725 | ||
|
|
73ccbe912f | ||
|
|
84e3a98303 | ||
|
|
7dd5b34d42 | ||
|
|
4470d0a704 | ||
|
|
a76e01d2b7 | ||
|
|
2697093ac1 | ||
|
|
59f9964e80 | ||
|
|
1516d9932b | ||
|
|
fcb115f42d | ||
|
|
e410272e6b | ||
|
|
87f1881b42 | ||
|
|
c0d85f3d85 | ||
|
|
98d79b8ed9 | ||
|
|
fe80730bb1 | ||
|
|
6c8ee96e6a | ||
|
|
b521dec8f9 | ||
|
|
e9baf0c4b6 | ||
|
|
e1a6f593e1 | ||
|
|
4b068b3058 | ||
|
|
da54bd6c21 | ||
|
|
0d179eca4d | ||
|
|
dacd511d24 | ||
|
|
c44b37c50c | ||
|
|
a721dc1f31 | ||
|
|
d2e6d23741 | ||
|
|
5f4a0317ab | ||
|
|
22f4be8f54 | ||
|
|
eeadc532fe | ||
|
|
93a35ad251 | ||
|
|
99787287bb | ||
|
|
bdd523190e | ||
|
|
4c1dd5c097 | ||
|
|
e1f658633d | ||
|
|
9c79105c02 | ||
|
|
6d5ceae8b4 | ||
|
|
381f09087a | ||
|
|
426b38bb33 | ||
|
|
488d98045e | ||
|
|
7955e0720b | ||
|
|
e017a19985 | ||
|
|
f8df76f526 | ||
|
|
11ebaec5f0 | ||
|
|
326b35a7ac | ||
|
|
5bf15548d0 | ||
|
|
6a734c0139 | ||
|
|
81b6f4d6f6 | ||
|
|
0b92d94570 | ||
|
|
fc5506179a | ||
|
|
0fe34ad224 | ||
|
|
54f35701a2 | ||
|
|
a809404ce1 | ||
|
|
fb32e44b47 | ||
|
|
e9c0369062 | ||
|
|
7358b3fe31 | ||
|
|
2a1f759e9e | ||
|
|
2fccb8c367 | ||
|
|
e039d95192 | ||
|
|
0f96031d6f | ||
|
|
6214fc84fa | ||
|
|
47578e02e3 | ||
|
|
35a4379b67 | ||
|
|
23f84642e6 | ||
|
|
edb9e85efd | ||
|
|
2d2c598fa6 | ||
|
|
cf4836dc75 | ||
|
|
d8306559fd | ||
|
|
e8c9d1c539 | ||
|
|
d8f415f8ab | ||
|
|
7b6579ac8a | ||
|
|
057307181e | ||
|
|
4fb832c042 | ||
|
|
e503cb69f2 | ||
|
|
95811e99bc | ||
|
|
62fff5ca60 | ||
|
|
5b28aa0848 | ||
|
|
db5aad8eb6 | ||
|
|
977ec33918 | ||
|
|
1819377897 | ||
|
|
f1b7bd59f6 | ||
|
|
f3afd5cb79 | ||
|
|
e6a5bf116e | ||
|
|
21b5a76fa7 | ||
|
|
b6263eb607 | ||
|
|
c8257e848e | ||
|
|
05bb7c8553 | ||
|
|
b600b11415 | ||
|
|
019ce80fc5 | ||
|
|
8cea2f75b3 | ||
|
|
6914063853 | ||
|
|
43e0d4a856 | ||
|
|
066d8e8d6c | ||
|
|
948e05c083 | ||
|
|
fb5b28d9cb | ||
|
|
677bce376b | ||
|
|
8faa96f5e6 | ||
|
|
f62806f6c9 | ||
|
|
58835b7e53 | ||
|
|
7a5298a755 | ||
|
|
bc4a6462ce | ||
|
|
ac3673e111 | ||
|
|
c746c1931d | ||
|
|
586d198d47 | ||
|
|
9515ceeb42 | ||
|
|
e8b4e9af46 | ||
|
|
10e399b3c3 | ||
|
|
dcbc3286e2 | ||
|
|
b185f9b56e | ||
|
|
7096b3dab9 | ||
|
|
36cacdf598 | ||
|
|
4e48ffc14d | ||
|
|
e119bc55ea | ||
|
|
1ce3068a99 | ||
|
|
d562d1a60d | ||
|
|
9f858398ab | ||
|
|
0ac80e8387 | ||
|
|
0dca0b92d1 | ||
|
|
c9b36ba32e | ||
|
|
f2c4e78381 | ||
|
|
05bff54b71 | ||
|
|
2bd163d92a | ||
|
|
5e27ba5c8c | ||
|
|
5aaeb3b76d | ||
|
|
36fb9f562a | ||
|
|
ad99bf1801 | ||
|
|
4c2a094255 | ||
|
|
97693cc611 | ||
|
|
c6d4fcd08f | ||
|
|
dd7b9ddd85 | ||
|
|
26d62e4117 | ||
|
|
babd7783af | ||
|
|
1529e796df | ||
|
|
d4b904b92b | ||
|
|
12d4177823 | ||
|
|
8142b32f38 | ||
|
|
c5abbb4e1c | ||
|
|
65ac73414f | ||
|
|
ede4213c8e | ||
|
|
b60d291490 | ||
|
|
b9ede79888 | ||
|
|
3d2cb838d1 | ||
|
|
778734419d | ||
|
|
be8683f556 | ||
|
|
c3450f4614 | ||
|
|
5881bc9ab0 | ||
|
|
a2fb499a20 | ||
|
|
411a928fea | ||
|
|
f5d02cdde9 | ||
|
|
c9340af8d0 | ||
|
|
a722bcc13f | ||
|
|
77fe3cfc60 | ||
|
|
470f93cefc | ||
|
|
92fde4dd12 | ||
|
|
95bc92955f | ||
|
|
f2f914221c | ||
|
|
c2d8038c63 | ||
|
|
cb8ac5ebf1 | ||
|
|
aa78e3ab1f | ||
|
|
bc00165094 | ||
|
|
94ef59602f | ||
|
|
14e2f84ceb | ||
|
|
f228fa5540 | ||
|
|
f2d2c1cbf8 | ||
|
|
d9be370e24 | ||
|
|
727c63b98e | ||
|
|
34dfb49b71 | ||
|
|
0b0a704d44 | ||
|
|
2d99d0bf13 | ||
|
|
1790df2090 | ||
|
|
10570ade44 | ||
|
|
43526d9d1a | ||
|
|
2636f876ab | ||
|
|
eed9da1471 | ||
|
|
9a2ebbabe2 | ||
|
|
716396a726 | ||
|
|
0727496601 | ||
|
|
194030fcfc | ||
|
|
b3b644527d | ||
|
|
7e5beeff46 | ||
|
|
a47b69bcec | ||
|
|
6ec6a23861 | ||
|
|
c9cc0d3d5d | ||
|
|
28d2b35718 | ||
|
|
b4f131be50 | ||
|
|
d0b359561f | ||
|
|
453636dfe2 | ||
|
|
b1605aa6d3 | ||
|
|
23503b80a4 | ||
|
|
0d69fbd9a3 | ||
|
|
0d665e528f | ||
|
|
de0b8bb7b2 | ||
|
|
84da110085 | ||
|
|
6b0d49b1fc | ||
|
|
4c20772e11 | ||
|
|
68f8348dde | ||
|
|
5023e77296 | ||
|
|
95316cbe8c | ||
|
|
cd454bae51 | ||
|
|
241201657c | ||
|
|
9eefaddd9b | ||
|
|
d6d47bbd6b | ||
|
|
82c883f95e | ||
|
|
dd40b0d9b9 | ||
|
|
963837ef1d | ||
|
|
66863b72f7 | ||
|
|
89773447a5 |
457 changed files with 146121 additions and 23871 deletions
|
|
@ -1,79 +0,0 @@
|
||||||
version: 2
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
docker:
|
|
||||||
- image: golangci/golangci-lint:v1.27.0
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: golangci-lint run -v
|
|
||||||
build-node:
|
|
||||||
docker:
|
|
||||||
- image: circleci/node
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: "Build"
|
|
||||||
command: ./wizard.sh -a
|
|
||||||
- run:
|
|
||||||
name: "Cleanup"
|
|
||||||
command: rm -rf frontend/node_modules
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths:
|
|
||||||
- '*'
|
|
||||||
build-go:
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:1.14.3
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: '~/project'
|
|
||||||
- run:
|
|
||||||
name: "Compile"
|
|
||||||
command: GOOS=linux GOARCH=amd64 ./wizard.sh -c
|
|
||||||
- run:
|
|
||||||
name: "Cleanup"
|
|
||||||
command: |
|
|
||||||
rm -rf frontend/build
|
|
||||||
git checkout -- go.sum # TODO: why is it being changed?
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths:
|
|
||||||
- '*'
|
|
||||||
release:
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:1.14.3
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: '~/project'
|
|
||||||
- setup_remote_docker
|
|
||||||
- run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
|
||||||
- run: curl -sL https://git.io/goreleaser | bash
|
|
||||||
- run: docker logout
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build-workflow:
|
|
||||||
jobs:
|
|
||||||
- lint:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /.*/
|
|
||||||
- build-node:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /.*/
|
|
||||||
- build-go:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /.*/
|
|
||||||
requires:
|
|
||||||
- build-node
|
|
||||||
- lint
|
|
||||||
- release:
|
|
||||||
context: deploy
|
|
||||||
requires:
|
|
||||||
- build-go
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /^v.*/
|
|
||||||
branches:
|
|
||||||
ignore: /.*/
|
|
||||||
160
.claude/CLAUDE.md
Normal file
160
.claude/CLAUDE.md
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Guidance for Claude when working in this repository (File Browser, `filebrowser/filebrowser`).
|
||||||
|
|
||||||
|
## Repo orientation
|
||||||
|
|
||||||
|
- Go backend (`github.com/filebrowser/filebrowser/v2`, ecosystem `go`) + Vue frontend under `frontend/`.
|
||||||
|
- The project is in **maintenance-only mode** (see `SECURITY.md`). Prefer small, surgical, well-tested changes.
|
||||||
|
- Version scheme: `v2.63.x`. Conventional-commit messages (`fix(scope): …`, `feat: …`, `chore: …`).
|
||||||
|
- Verify with `go build ./...`, `go vet ./...`, `go test ./...`. Reuse existing test harnesses (e.g. `signToken`, `scopedUserStorage`, `handle`, `customFSUser`, `mockUserStore`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Handling security advisories
|
||||||
|
|
||||||
|
Use this playbook when asked to triage, verify, fix, or manage GitHub security advisories.
|
||||||
|
All advisory state lives on GitHub and is driven with the `gh` CLI. States are
|
||||||
|
`triage → draft → published`, plus `closed`.
|
||||||
|
|
||||||
|
## 1. Fetch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List by state (also: published, draft, closed)
|
||||||
|
gh api '/repos/filebrowser/filebrowser/security-advisories?state=triage&per_page=100' \
|
||||||
|
--jq '.[] | {ghsa_id, severity, summary, state}'
|
||||||
|
|
||||||
|
# Full report for one advisory (read .summary and .description)
|
||||||
|
gh api /repos/filebrowser/filebrowser/security-advisories/GHSA-xxxx-xxxx-xxxx \
|
||||||
|
--jq '.summary, "---", .description'
|
||||||
|
```
|
||||||
|
|
||||||
|
Always pull the **published** set too — you need it to dedup against.
|
||||||
|
|
||||||
|
## 2. Verify each report — do NOT trust the report text
|
||||||
|
|
||||||
|
Read the actual source **at HEAD** and reach one verdict per advisory:
|
||||||
|
|
||||||
|
- **CONFIRMED** — defect exists at HEAD. Quote the exact `file:line`.
|
||||||
|
- **FIXED** — already patched (find the fix commit).
|
||||||
|
- **FALSE / NOT APPLICABLE** — claim is wrong, or targets a different project.
|
||||||
|
- **NOT EXPLOITABLE** — pattern exists but no code path can reach the precondition.
|
||||||
|
- **DUPLICATE** — of a published advisory, or of another triage advisory.
|
||||||
|
|
||||||
|
Common traps — check each before accepting a report:
|
||||||
|
|
||||||
|
- **"Incomplete fix of a prior advisory."** Read the original fix commit and confirm the *specific*
|
||||||
|
sibling code path is actually still unguarded — a prior fix may already cover a related path.
|
||||||
|
- **Wrong project.** Confirm the referenced files, symbols, and endpoints exist in this repo; reports
|
||||||
|
sometimes describe a fork. If the cited code isn't here, close as not applicable.
|
||||||
|
- **"Legacy / upgraded / imported records are affected."** Trace the field's git history and confirm that
|
||||||
|
some released version could actually produce such a record before believing it
|
||||||
|
(`git log -S'Field' -- path`, `git show <commit>`, `git tag --contains <commit>`).
|
||||||
|
- **Overlapping reports.** Two triage advisories may share one root cause — consolidate and fix once.
|
||||||
|
- **Known, intentionally-unaddressed classes.** Some areas are known and tracked but not fixed (see
|
||||||
|
`SECURITY.md`'s Known Issues); matching reports are duplicates.
|
||||||
|
|
||||||
|
Record, per advisory: verdict, the `file:line` evidence, exploitability preconditions
|
||||||
|
(default config? platform-specific? auth required?), and the disposition.
|
||||||
|
|
||||||
|
## 3. Fix (one commit per advisory)
|
||||||
|
|
||||||
|
- Branch off `master` first; never commit fixes directly to `master`.
|
||||||
|
- **One commit per fix**, each referencing the GHSA in the body (`Refs GHSA-xxxx-xxxx-xxxx`).
|
||||||
|
- When several advisories share a root cause because parallel code paths diverged, **centralize** the logic
|
||||||
|
(e.g. `settings.CreateUserHome` used by signup, proxy, and hook provisioning) so they cannot drift again.
|
||||||
|
- Keep it surgical; match surrounding style.
|
||||||
|
|
||||||
|
## 4. Add a regression test per fix
|
||||||
|
|
||||||
|
- Reuse the existing harnesses; add a focused test asserting the fixed behavior (and that the legitimate
|
||||||
|
path still works). Commit tests alongside the fixes (`test(scope): …`, `Refs GHSA-…`).
|
||||||
|
- Run `go test ./... && go vet ./...` before finishing.
|
||||||
|
|
||||||
|
## 5. Severity — set a CVSS vector, don't hand-assert
|
||||||
|
|
||||||
|
Set a CVSS v3.1 vector; GitHub derives the score **and** severity from it (overriding the plain `severity`
|
||||||
|
field). Encode the real preconditions in the vector so the band is defensible:
|
||||||
|
|
||||||
|
- Requires a specific target configuration/platform (e.g. case-insensitive FS) → **AC:H**.
|
||||||
|
- Unauthenticated vs needs an account → **PR:N / PR:L**.
|
||||||
|
- Keep it consistent with the **predecessor CVE's** rating (an incomplete-fix follow-up should not outrank
|
||||||
|
its parent). Bands: `0.1–3.9` low, `4.0–6.9` medium, `7.0–8.9` high, `9.0–10.0` critical.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh api -X PATCH /repos/filebrowser/filebrowser/security-advisories/GHSA-xxxx-xxxx-xxxx \
|
||||||
|
-f cvss_vector_string='CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H' \
|
||||||
|
--jq '{ghsa_id, severity, score: .cvss.score, vector: .cvss.vector_string}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Clean up the title
|
||||||
|
|
||||||
|
Rewrite reporter titles to the concise, sentence-case, backtick-free style of the published advisories
|
||||||
|
(state the vuln; drop "Incomplete fix of…" prefixes and jargon). The title is the `summary` field:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx \
|
||||||
|
-f summary='Proxy-auth auto-provisioning ignores createUserDir and grants the server root scope'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Rewrite the body into the standard structure
|
||||||
|
|
||||||
|
Reporter reports arrive in whatever shape the reporter used. Before drafting, rewrite the
|
||||||
|
`description` into the sections below — **reusing the reporter's own wording wherever it is
|
||||||
|
accurate**, rather than paraphrasing it. Drop the greeting, the offer to help, and any claim the
|
||||||
|
verification in step 2 disproved. Keep sections in this order and omit the ones that don't apply:
|
||||||
|
|
||||||
|
| Section | Contents |
|
||||||
|
| --- | --- |
|
||||||
|
| `## Summary` | What the defect is, in two or three sentences. Note the version it was reported against and the range it was verified over. |
|
||||||
|
| `## Details` | Root cause, naming `file.go`, the function, and a short quote of the **pre-fix** code. |
|
||||||
|
| `## PoC` | The reporter's reproduction steps and observed result, trimmed to the essentials. |
|
||||||
|
| `## Impact` | Who can exploit it (privilege level, preconditions) and what they get. |
|
||||||
|
| `## Patches` | Fixed version plus a link to the fix commit, and one sentence on what the fix does. Mention it if the reporter re-tested and confirmed. |
|
||||||
|
| `## Workarounds` | Real mitigations, or `None. Upgrade to vX.Y.Z.` |
|
||||||
|
| `## Out of scope` | Anything in the original report deliberately **not** treated as a vulnerability, with the reasoning. Needed whenever the advisory is narrower than the report. |
|
||||||
|
| `## References` | Fix and regression-test commit links. |
|
||||||
|
|
||||||
|
Use `##` headings (matching the published advisories) and keep the body in the maintainer's voice —
|
||||||
|
first person ("I reproduced…") belongs only inside quoted PoC steps.
|
||||||
|
|
||||||
|
Send it as a file so the Markdown survives shell quoting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jq -Rs '{description: .}' desc.md \
|
||||||
|
| gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx --input -
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Set affected & patched versions
|
||||||
|
|
||||||
|
The package is always `{ecosystem: "go", name: "github.com/filebrowser/filebrowser/v2"}`.
|
||||||
|
|
||||||
|
- `vulnerable_version_range` — `<= <latest released version>` (the newest tag; find it with
|
||||||
|
`git tag --list 'v2.*' --sort=-version:refname | head -1`).
|
||||||
|
- `patched_versions` — the **next** release that will actually ship the fix. It doesn't exist just
|
||||||
|
because the branch does; it still has to be cut. **When unsure which version that will be, ask.**
|
||||||
|
|
||||||
|
Replace the placeholder versions below before sending:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '%s' '{"vulnerabilities":[{"package":{"ecosystem":"go","name":"github.com/filebrowser/filebrowser/v2"},"vulnerable_version_range":"<= <latest>","patched_versions":"<next>","vulnerable_functions":[]}]}' \
|
||||||
|
| gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx --input -
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Move state / close, by disposition
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx -f state=draft # fixed, awaiting release
|
||||||
|
gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx -f state=closed # duplicate / N-A / not-exploitable
|
||||||
|
```
|
||||||
|
|
||||||
|
- **CONFIRMED & fixed** → metadata done → **draft**. Publish after the patched release ships.
|
||||||
|
- **DUPLICATE / NOT APPLICABLE / NOT EXPLOITABLE** → **closed**.
|
||||||
|
- **DEFERRED** (confirmed but not yet fixed) → leave in **triage** with a note.
|
||||||
|
- The REST API **cannot post advisory comments** — replies to reporters (dup notice, evidence, links to
|
||||||
|
the relevant tracking issue) must be posted manually in the advisory UI. Draft the text for the maintainer.
|
||||||
|
|
||||||
|
## 10. Release & publish
|
||||||
|
|
||||||
|
Push the branch, open a PR, merge, tag/release the `patched_versions` you set, then publish the drafts.
|
||||||
|
Confirm outward-facing/irreversible advisory actions (close, publish) with the maintainer before doing them.
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
testdata/
|
.venv
|
||||||
.github/
|
dist
|
||||||
**.git
|
.idea
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
filebrowser.db
|
||||||
|
docs/index.md
|
||||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
* @filebrowser/maintainers
|
||||||
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,22 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
---
|
|
||||||
|
|
||||||
**Description**
|
|
||||||
A clear and concise description of what the issue is about. What are you trying to do?
|
|
||||||
|
|
||||||
**Expected behaviour**
|
|
||||||
What did you expect to happen?
|
|
||||||
|
|
||||||
**What is happening instead?**
|
|
||||||
Please, give full error messages and/or log.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here. If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**How to reproduce?**
|
|
||||||
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible?
|
|
||||||
|
|
||||||
**Files**
|
|
||||||
A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile.
|
|
||||||
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
name: Bug Report
|
||||||
|
description: Report a bug in FileBrowser.
|
||||||
|
labels: [bug, 'waiting: triage']
|
||||||
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Checklist
|
||||||
|
description: Please verify that you've followed these steps
|
||||||
|
options:
|
||||||
|
- label: This is a bug report, not a question.
|
||||||
|
required: true
|
||||||
|
- label: I have searched on the [issue tracker](https://github.com/filebrowser/filebrowser/issues?q=is%3Aissue) for my bug.
|
||||||
|
required: true
|
||||||
|
- label: I am running the latest [FileBrowser version](https://github.com/filebrowser/filebrowser/releases) or have an issue updating.
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
render: Text
|
||||||
|
description: |
|
||||||
|
Enter the version of FileBrowser you are using.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: |
|
||||||
|
A clear and concise description of what the issue is about. What are you trying to do?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: What actually happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Reproduction Steps
|
||||||
|
description: |
|
||||||
|
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Files
|
||||||
|
description: |
|
||||||
|
A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile.
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: GitHub Discussions
|
||||||
|
url: https://github.com/filebrowser/filebrowser/discussions
|
||||||
|
about: Please ask questions and discuss features here.
|
||||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,16 +0,0 @@
|
||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]*
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
Add a clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
Add a clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
24
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,16 +1,16 @@
|
||||||
**Description**
|
## Description
|
||||||
Please explain the changes you made here.
|
|
||||||
If the feature changes current behaviour, explain why your solution is better.
|
|
||||||
|
|
||||||
:rotating_light: Before submitting your PR, please read [community](https://github.com/filebrowser/community), and indicate which issues (in any of the repos) are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
|
<!-- Please explain the changes you made here. -->
|
||||||
|
|
||||||
- [ ] DO make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master!
|
## Additional Information
|
||||||
- [ ] DO make sure you are making a pull request against the **master branch** (left side). Also you should start *your branch* off *our master*.
|
|
||||||
- [ ] DO make sure that File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md).
|
|
||||||
- [ ] DO make sure that related issues are opened in other repositories. I.e., the frontend, caddy plugins or the web page need to be updated accordingly.
|
|
||||||
- [ ] AVOID breaking the continuous integration build.
|
|
||||||
|
|
||||||
**Further comments**
|
<!-- If it is a relatively large or complex change, please add more information to explain what you did, how you did it, if you considered any alternatives, etc. -->
|
||||||
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc.
|
|
||||||
|
|
||||||
:heart: Thank you!
|
## Checklist
|
||||||
|
|
||||||
|
Before submitting your PR, please indicate which issues are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
|
||||||
|
|
||||||
|
- [ ] I am aware this project is being **archived on 2026-09-01** and that no further changes will be merged, including bug fixes. See [README](https://github.com/filebrowser/filebrowser/blob/master/README.md)
|
||||||
|
- [ ] I am aware that translations MUST be made through [Transifex](https://app.transifex.com/file-browser/file-browser/) and that this PR is NOT a translation update
|
||||||
|
- [ ] I am making a PR against the `master` branch.
|
||||||
|
- [ ] I am sure File Browser can be successfully built. See [CONTRIBUTING.md](https://github.com/filebrowser/filebrowser/blob/master/CONTRIBUTING.md).
|
||||||
|
|
|
||||||
133
.github/workflows/ci.yaml
vendored
Normal file
133
.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
name: Continuous Integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "master"
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-frontend:
|
||||||
|
name: Lint Frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
package_json_file: "frontend/package.json"
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: "24.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
|
- working-directory: frontend
|
||||||
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm run lint
|
||||||
|
|
||||||
|
test-frontend:
|
||||||
|
name: Test Frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
package_json_file: "frontend/package.json"
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: "24.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
|
- working-directory: frontend
|
||||||
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm run test
|
||||||
|
|
||||||
|
lint-backend:
|
||||||
|
name: Lint Backend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "1.26.x"
|
||||||
|
- uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: "latest"
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "1.26.x"
|
||||||
|
- run: go test --race ./...
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
package_json_file: "frontend/package.json"
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: "24.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
|
- name: Install Task
|
||||||
|
uses: go-task/setup-task@v2
|
||||||
|
- run: task build
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
needs: ["lint-frontend", "lint-backend", "test", "build"]
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
package_json_file: "frontend/package.json"
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: "24.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
- name: Install Task
|
||||||
|
uses: go-task/setup-task@v2
|
||||||
|
- run: task build:frontend
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v7
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
46
.github/workflows/lint-pr.yaml
vendored
Normal file
46
.github/workflows/lint-pr.yaml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: "Lint PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
name: Validate Title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v6
|
||||||
|
id: lint_pr_title
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: marocchino/sticky-pull-request-comment@v3
|
||||||
|
# When the previous steps fails, the workflow would stop. By adding this
|
||||||
|
# condition you can continue the execution with the populated error message.
|
||||||
|
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
message: |
|
||||||
|
Hey there and thank you for opening this pull request! 👋🏼
|
||||||
|
|
||||||
|
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
```
|
||||||
|
${{ steps.lint_pr_title.outputs.error_message }}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Delete a previous comment when the issue has been resolved
|
||||||
|
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||||
|
uses: marocchino/sticky-pull-request-comment@v3
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
delete: true
|
||||||
17
.gitignore
vendored
17
.gitignore
vendored
|
|
@ -1,15 +1,15 @@
|
||||||
*.db
|
*.db
|
||||||
*.lock
|
|
||||||
*.bak
|
*.bak
|
||||||
_old
|
_old
|
||||||
rice-box.go
|
rice-box.go
|
||||||
.idea/
|
.idea/
|
||||||
filebrowser
|
/filebrowser
|
||||||
dist/
|
/filebrowser.exe
|
||||||
|
/dist
|
||||||
|
.venv
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/frontend/dist
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
|
|
@ -28,3 +28,12 @@ yarn-error.log*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw*
|
*.sw*
|
||||||
|
bin/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Vue distributable files
|
||||||
|
/frontend/dist/*
|
||||||
|
!/frontend/dist/.gitkeep
|
||||||
|
|
||||||
|
default.nix
|
||||||
|
Dockerfile.dev
|
||||||
|
|
|
||||||
136
.golangci.yml
136
.golangci.yml
|
|
@ -1,132 +1,14 @@
|
||||||
linters-settings:
|
version: "2"
|
||||||
dupl:
|
|
||||||
threshold: 100
|
|
||||||
exhaustive:
|
|
||||||
default-signifies-exhaustive: false
|
|
||||||
funlen:
|
|
||||||
lines: 100
|
|
||||||
statements: 50
|
|
||||||
goconst:
|
|
||||||
min-len: 2
|
|
||||||
min-occurrences: 2
|
|
||||||
gocritic:
|
|
||||||
enabled-tags:
|
|
||||||
- diagnostic
|
|
||||||
- experimental
|
|
||||||
- opinionated
|
|
||||||
- performance
|
|
||||||
- style
|
|
||||||
disabled-checks:
|
|
||||||
- dupImport # https://github.com/go-critic/go-critic/issues/845
|
|
||||||
- ifElseChain
|
|
||||||
- octalLiteral
|
|
||||||
- whyNoLint
|
|
||||||
- wrapperFunc
|
|
||||||
gocyclo:
|
|
||||||
min-complexity: 15
|
|
||||||
goimports:
|
|
||||||
local-prefixes: github.com/filebrowser/filebrowser
|
|
||||||
golint:
|
|
||||||
min-confidence: 0
|
|
||||||
gomnd:
|
|
||||||
settings:
|
|
||||||
mnd:
|
|
||||||
# don't include the "operation" and "assign"
|
|
||||||
checks: argument,case,condition,return
|
|
||||||
govet:
|
|
||||||
check-shadowing: true
|
|
||||||
lll:
|
|
||||||
line-length: 140
|
|
||||||
maligned:
|
|
||||||
suggest-new: true
|
|
||||||
misspell:
|
|
||||||
locale: US
|
|
||||||
nolintlint:
|
|
||||||
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
|
|
||||||
allow-unused: false # report any unused nolint directives
|
|
||||||
require-explanation: false # don't require an explanation for nolint directives
|
|
||||||
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
default: standard
|
||||||
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
|
||||||
disable-all: true
|
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
|
||||||
- deadcode
|
|
||||||
- depguard
|
|
||||||
- dogsled
|
|
||||||
- dupl
|
|
||||||
- errcheck
|
|
||||||
- funlen
|
|
||||||
- gochecknoinits
|
|
||||||
- goconst
|
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
|
||||||
- gofmt
|
|
||||||
- goimports
|
|
||||||
- golint
|
|
||||||
- gomnd
|
|
||||||
- goprintffuncname
|
|
||||||
- gosec
|
|
||||||
- gosimple
|
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- revive
|
||||||
- interfacer
|
exclusions:
|
||||||
- lll
|
presets:
|
||||||
- misspell
|
- std-error-handling
|
||||||
- nakedret
|
- comments
|
||||||
- nolintlint
|
paths:
|
||||||
- rowserrcheck
|
- frontend/
|
||||||
- scopelint
|
|
||||||
- staticcheck
|
|
||||||
- structcheck
|
|
||||||
- stylecheck
|
|
||||||
- typecheck
|
|
||||||
- unconvert
|
|
||||||
- unparam
|
|
||||||
- unused
|
|
||||||
- varcheck
|
|
||||||
- whitespace
|
|
||||||
- prealloc
|
|
||||||
|
|
||||||
# don't enable:
|
|
||||||
# - asciicheck
|
|
||||||
# - exhaustive (TODO: enable after next release; current release at time of writing is v1.27)
|
|
||||||
# - gochecknoglobals
|
|
||||||
# - gocognit
|
|
||||||
# - godot
|
|
||||||
# - godox
|
|
||||||
# - goerr113
|
|
||||||
# - maligned
|
|
||||||
# - nestif
|
|
||||||
# - testpackage
|
|
||||||
# - wsl
|
|
||||||
|
|
||||||
issues:
|
|
||||||
exclude-rules:
|
|
||||||
- path: cmd/.*.go
|
|
||||||
linters:
|
|
||||||
- gochecknoinits
|
|
||||||
- path: .*_test.go
|
|
||||||
linters:
|
|
||||||
- lll
|
|
||||||
- gochecknoinits
|
|
||||||
- gocyclo
|
|
||||||
- funlen
|
|
||||||
- dupl
|
|
||||||
- scopelint
|
|
||||||
- text: "Auther"
|
|
||||||
linters:
|
|
||||||
- misspell
|
|
||||||
|
|
||||||
run:
|
|
||||||
skip-dirs:
|
|
||||||
- frontend/
|
|
||||||
skip-files:
|
|
||||||
- http/rice-box.go
|
|
||||||
|
|
||||||
# golangci.com configuration
|
|
||||||
# https://github.com/golangci/golangci/wiki/Configuration
|
|
||||||
service:
|
|
||||||
golangci-lint-version: 1.27.x # use the fixed version to not introduce new linters unexpectedly
|
|
||||||
|
|
|
||||||
247
.goreleaser.yml
247
.goreleaser.yml
|
|
@ -1,81 +1,204 @@
|
||||||
|
version: 2
|
||||||
|
|
||||||
project_name: filebrowser
|
project_name: filebrowser
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- GO111MODULE=on
|
- GO111MODULE=on
|
||||||
|
|
||||||
before:
|
builds:
|
||||||
hooks:
|
- env:
|
||||||
- go mod download
|
- CGO_ENABLED=0
|
||||||
|
ldflags:
|
||||||
build:
|
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
|
||||||
env:
|
main: main.go
|
||||||
- CGO_ENABLED=0
|
binary: filebrowser
|
||||||
ldflags:
|
goos:
|
||||||
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
|
- darwin
|
||||||
main: main.go
|
- linux
|
||||||
binary: filebrowser
|
- windows
|
||||||
goos:
|
- freebsd
|
||||||
- darwin
|
- openbsd
|
||||||
- linux
|
goarch:
|
||||||
- windows
|
- amd64
|
||||||
- freebsd
|
- "386"
|
||||||
- netbsd
|
- arm
|
||||||
- openbsd
|
- arm64
|
||||||
- dragonfly
|
- riscv64
|
||||||
- solaris
|
goarm:
|
||||||
goarch:
|
- "5"
|
||||||
- amd64
|
- "6"
|
||||||
- 386
|
- "7"
|
||||||
- arm
|
ignore:
|
||||||
- arm64
|
- goos: darwin
|
||||||
goarm:
|
goarch: "386"
|
||||||
- 5
|
# Experimental, may not work properly
|
||||||
- 6
|
- goos: openbsd
|
||||||
- 7
|
goarch: riscv64
|
||||||
ignore:
|
# Broken as of Go 1.24, deprecated as of Go 1.26
|
||||||
- goos: darwin
|
- goos: windows
|
||||||
goarch: 386
|
goarch: arm
|
||||||
- goos: openbsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: freebsd
|
|
||||||
goarch: arm
|
|
||||||
- goos: netbsd
|
|
||||||
goarch: arm
|
|
||||||
- goos: solaris
|
|
||||||
goarch: arm
|
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
-
|
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||||
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
formats: ["tar.gz"]
|
||||||
format: tar.gz
|
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats: ["zip"]
|
||||||
|
|
||||||
dockers:
|
dockers:
|
||||||
-
|
# Alpine docker images
|
||||||
dockerfile: Dockerfile
|
- dockerfile: Dockerfile
|
||||||
binaries:
|
use: buildx
|
||||||
- filebrowser
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/amd64"
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
goarm: ''
|
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:latest"
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}"
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}"
|
|
||||||
extra_files:
|
extra_files:
|
||||||
- .docker.json
|
- docker
|
||||||
-
|
- dockerfile: Dockerfile
|
||||||
dockerfile: Dockerfile
|
use: buildx
|
||||||
binaries:
|
build_flag_templates:
|
||||||
- filebrowser
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
||||||
|
extra_files:
|
||||||
|
- docker
|
||||||
|
- dockerfile: Dockerfile
|
||||||
|
use: buildx
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm/v6"
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: '5'
|
goarm: "6"
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:pi"
|
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-pi"
|
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-pi"
|
|
||||||
extra_files:
|
extra_files:
|
||||||
- .docker.json
|
- docker
|
||||||
|
- dockerfile: Dockerfile
|
||||||
|
use: buildx
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm/v7"
|
||||||
|
goos: linux
|
||||||
|
goarch: arm
|
||||||
|
goarm: "7"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
||||||
|
extra_files:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
## s6-overlay docker images
|
||||||
|
- dockerfile: Dockerfile.s6
|
||||||
|
use: buildx
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||||
|
extra_files:
|
||||||
|
- docker
|
||||||
|
- dockerfile: Dockerfile.s6
|
||||||
|
use: buildx
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
||||||
|
extra_files:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
docker_manifests:
|
||||||
|
- name_template: "filebrowser/filebrowser:latest"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||||
|
- name_template: "filebrowser/filebrowser:{{ .Tag }}"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||||
|
- name_template: "filebrowser/filebrowser:v{{ .Major }}"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
||||||
|
## s6 image manifests
|
||||||
|
- name_template: "filebrowser/filebrowser:s6"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
||||||
|
- name_template: "filebrowser/filebrowser:{{ .Tag }}-s6"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
||||||
|
- name_template: "filebrowser/filebrowser:v{{ .Major }}-s6"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
||||||
|
|
||||||
|
homebrew_casks:
|
||||||
|
- name: filebrowser
|
||||||
|
repository:
|
||||||
|
owner: filebrowser
|
||||||
|
name: homebrew-tap
|
||||||
|
commit_author:
|
||||||
|
name: FileBrowser Robot
|
||||||
|
email: robot@filebrowser.org
|
||||||
|
homepage: https://github.com/filebrowser/filebrowser
|
||||||
|
description: File Browser is a create-your-own-cloud-kind of software where you can install it on a server, direct it to a path and then access your files through a nice web interface
|
||||||
|
hooks:
|
||||||
|
post:
|
||||||
|
install: |
|
||||||
|
if system_command("/usr/bin/xattr", args: ["-h"]).exit_status == 0
|
||||||
|
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/filebrowser"]
|
||||||
|
end
|
||||||
|
|
|
||||||
10
.tx/config
10
.tx/config
|
|
@ -1,10 +0,0 @@
|
||||||
[main]
|
|
||||||
host = https://www.transifex.com
|
|
||||||
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se
|
|
||||||
|
|
||||||
[file-browser.file-browser]
|
|
||||||
file_filter = frontend/src/i18n/<lang>.json
|
|
||||||
minimum_perc = 50
|
|
||||||
source_file = frontend/src/i18n/en.json
|
|
||||||
source_lang = en
|
|
||||||
type = KEYVALUEJSON
|
|
||||||
14
.versionrc
Normal file
14
.versionrc
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"types": [
|
||||||
|
{ "type": "feat", "section": "Features" },
|
||||||
|
{ "type": "fix", "section": "Bug Fixes" },
|
||||||
|
{ "type": "perf", "section": "Performance improvements" },
|
||||||
|
{ "type": "revert", "section": "Reverts" },
|
||||||
|
{ "type": "refactor", "section": "Refactorings" },
|
||||||
|
{ "type": "build", "section": "Build" },
|
||||||
|
{ "type": "ci", "hidden": true },
|
||||||
|
{ "type": "test", "hidden": true },
|
||||||
|
{ "type": "chore", "hidden": true },
|
||||||
|
{ "type": "docs", "hidden": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
1931
CHANGELOG.md
Normal file
1931
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
46
CODE-OF-CONDUCT.md
Normal file
46
CODE-OF-CONDUCT.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
### Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
### Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
### Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
### Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hacdias@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||||
|
|
||||||
|
### Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4, available at [https://contributor-covenant.org/version/1/4](https://contributor-covenant.org/version/1/4).
|
||||||
|
|
||||||
115
CONTRIBUTING.md
Normal file
115
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
# Building File Browser
|
||||||
|
|
||||||
|
This project is archived on 2026-09-01. Pull requests are not accepted and no further changes are merged. This document is kept as build documentation for anyone wishing to build the project from source.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
The backend side of the application is written in [Go](https://golang.org/), while the frontend (located on a subdirectory of the same name) is written in [Vue.js](https://vuejs.org/). Due to the tight coupling required by some features, basic knowledge of both Go and Vue.js is recommended.
|
||||||
|
|
||||||
|
* Learn Go: [https://github.com/golang/go/wiki/Learn](https://github.com/golang/go/wiki/Learn)
|
||||||
|
* Learn Vue.js: [https://vuejs.org/guide/introduction.html](https://vuejs.org/guide/introduction.html)
|
||||||
|
|
||||||
|
We encourage you to use git to manage your fork. To clone the main repository, just run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/filebrowser/filebrowser
|
||||||
|
```
|
||||||
|
|
||||||
|
We use [Taskfile](https://taskfile.dev/) to manage the different processes (building, releasing, etc) automatically.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
You can fully build the project in order to produce a binary by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
For development, there are a few things to have in mind.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
We use [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. Prepare the frontend environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the root of the repo, go to frontend/
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# Install the dependencies
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
If you just want to develop the backend, you can create a static build of the frontend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to develop the frontend, start a development server which watches for changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that you need to access File Browser's interface through the development server of the frontend.
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
First prepare the backend environment by downloading all required dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now build or run File Browser as any other Go project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
go build
|
||||||
|
|
||||||
|
# Run
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Documentation lives in [`docs`](docs) as plain Markdown and is no longer built into a site. The command line reference in [`docs/cli`](docs/cli) is generated from the commands themselves:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task docs:cli:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
To make a release, just run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
task release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
The Transifex integration stopped on 2026-09-01 and translations submitted there no longer reach this repository. Locale files live in [`frontend/src/i18n`](frontend/src/i18n) and can be edited directly.
|
||||||
|
|
||||||
|
## Authentication Provider
|
||||||
|
|
||||||
|
To build a new authentication provider, you need to implement the [Auther interface](https://github.com/filebrowser/filebrowser/blob/master/auth/auth.go), whose method will be called on the login page after the user has submitted their login data.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Auther is the authentication interface.
|
||||||
|
type Auther interface {
|
||||||
|
// Auth is called to authenticate a request.
|
||||||
|
Auth(r *http.Request, s *users.Storage, root string) (*users.User, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After implementing the interface you should:
|
||||||
|
|
||||||
|
1. Add it to [`auth` directory](https://github.com/filebrowser/filebrowser/blob/master/auth).
|
||||||
|
2. Add it to the [configuration parser](https://github.com/filebrowser/filebrowser/blob/master/cmd/config.go) for the CLI.
|
||||||
|
3. Add it to the [`authBackend.Get`](https://github.com/filebrowser/filebrowser/blob/master/storage/bolt/auth.go).
|
||||||
|
|
||||||
|
If you need to add more flags, please update the function `addConfigFlags`.
|
||||||
|
|
||||||
53
Dockerfile
53
Dockerfile
|
|
@ -1,15 +1,46 @@
|
||||||
FROM alpine:latest as alpine
|
## Multistage build: First stage fetches dependencies
|
||||||
RUN apk --update add ca-certificates
|
FROM alpine:3.23 AS fetcher
|
||||||
RUN apk --update add mailcap
|
|
||||||
|
|
||||||
FROM scratch
|
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
|
||||||
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
RUN apk update && \
|
||||||
COPY --from=alpine /etc/mime.types /etc/mime.types
|
apk --no-cache add ca-certificates mailcap tini-static && \
|
||||||
|
wget -O /JSON.sh https://raw.githubusercontent.com/dominictarr/JSON.sh/0d5e5c77365f63809bf6e77ef44a1f34b0e05840/JSON.sh
|
||||||
|
|
||||||
|
## Second stage: Use lightweight BusyBox image for final runtime environment
|
||||||
|
FROM busybox:1.37.0-musl
|
||||||
|
|
||||||
|
# Define non-root user UID and GID
|
||||||
|
ENV UID=1000
|
||||||
|
ENV GID=1000
|
||||||
|
|
||||||
|
# Create user group and user
|
||||||
|
RUN addgroup -g $GID user && \
|
||||||
|
adduser -D -u $UID -G user user
|
||||||
|
|
||||||
|
# Copy binary, scripts, and configurations into image with proper ownership
|
||||||
|
COPY --chown=user:user filebrowser /bin/filebrowser
|
||||||
|
COPY --chown=user:user docker/common/ /
|
||||||
|
COPY --chown=user:user docker/alpine/ /
|
||||||
|
COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini
|
||||||
|
COPY --from=fetcher /JSON.sh /JSON.sh
|
||||||
|
COPY --from=fetcher /etc/ca-certificates.conf /etc/ca-certificates.conf
|
||||||
|
COPY --from=fetcher /etc/ca-certificates /etc/ca-certificates
|
||||||
|
COPY --from=fetcher /etc/mime.types /etc/mime.types
|
||||||
|
COPY --from=fetcher /etc/ssl /etc/ssl
|
||||||
|
|
||||||
|
# Create data directories, set ownership, and ensure healthcheck script is executable
|
||||||
|
RUN mkdir -p /config /database /srv && \
|
||||||
|
chown -R user:user /config /database /srv \
|
||||||
|
&& chmod +x /healthcheck.sh
|
||||||
|
|
||||||
|
# Define healthcheck script
|
||||||
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
||||||
|
|
||||||
|
# Set the user, volumes and exposed ports
|
||||||
|
USER user
|
||||||
|
|
||||||
|
VOLUME /srv /config /database
|
||||||
|
|
||||||
VOLUME /srv
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
COPY .docker.json /.filebrowser.json
|
ENTRYPOINT [ "tini", "--", "/init.sh" ]
|
||||||
COPY filebrowser /filebrowser
|
|
||||||
|
|
||||||
ENTRYPOINT [ "/filebrowser" ]
|
|
||||||
|
|
|
||||||
24
Dockerfile.s6
Normal file
24
Dockerfile.s6
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
FROM ghcr.io/linuxserver/baseimage-alpine:3.23
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
apk --no-cache add ca-certificates mailcap jq libcap
|
||||||
|
|
||||||
|
# Make user and create necessary directories
|
||||||
|
RUN mkdir -p /config /database /srv && \
|
||||||
|
chown -R abc:abc /config /database /srv
|
||||||
|
|
||||||
|
# Copy files and set permissions
|
||||||
|
COPY filebrowser /bin/filebrowser
|
||||||
|
COPY docker/common/ /
|
||||||
|
COPY docker/s6/ /
|
||||||
|
|
||||||
|
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh && \
|
||||||
|
setcap 'cap_net_bind_service=+ep' /bin/filebrowser
|
||||||
|
|
||||||
|
# Define healthcheck script
|
||||||
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
||||||
|
|
||||||
|
# Set the volumes and exposed ports
|
||||||
|
VOLUME /srv /config /database
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -187,7 +187,7 @@
|
||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2018 File Browser contributors
|
Copyright 2018 File Browser Contributors
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
||||||
39
README.md
39
README.md
|
|
@ -1,29 +1,36 @@
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> **File Browser is archived on 2026-09-01**. The last planned release has already shipped. There will be no further releases, bug fixes, or security fixes.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
<img src="./branding/banner.png" width="550"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|
File Browser provides a file managing interface within a specified directory and it can be used to upload, delete, preview and edit your files. It is a **create-your-own-cloud**-kind of software where you can just install it on your server, direct it to a path and access your files through a nice web interface.
|
||||||
|
|
||||||
[](https://travis-ci.com/filebrowser/filebrowser)
|
**Background:** [Goodbye File Browser, for Real This Time](https://hacdias.com/2026/07/28/filebrowser/), July 2026.
|
||||||
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
|
|
||||||
[](http://godoc.org/github.com/filebrowser/filebrowser)
|
|
||||||
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
|
||||||
[](http://webchat.freenode.net/?channels=%23filebrowser)
|
|
||||||
|
|
||||||
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app or as a middleware.
|
## Security
|
||||||
|
|
||||||
## Features
|
Published advisories are listed under [security advisories](https://github.com/filebrowser/filebrowser/security/advisories),
|
||||||
|
and reporting instructions are in [SECURITY.md](SECURITY.md). Two known issue classes
|
||||||
|
remain unaddressed and will not be fixed:
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz/features](https://github.com/filebrowser/docs/tree/master/features)
|
- **Command execution, runner, and hooks.** This feature is plagued with vulnerabilities across many published advisories, and would need a full rewrite to be made safe. It is disabled by default; if you re-enable it with `--disable-exec=false`, treat the ability to run commands as equivalent to shell access on the host. Background: [#5199](https://github.com/filebrowser/filebrowser/issues/5199).
|
||||||
|
- **Session and JWT handling.** Sessions are self-contained JWTs rather than server-side identifiers, so they cannot be revoked, which means that logout, password changes, and renewal leave previously issued tokens valid until they expire, and the same refresh token can be redeemed repeatedly. Assume a leaked token is valid until expiry. Background: [#5216](https://github.com/filebrowser/filebrowser/issues/5216).
|
||||||
|
|
||||||
## Install
|
If you keep running File Browser, treat it as unmaintained software:
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz](https://github.com/filebrowser/docs/tree/master/).
|
- **Do not expose it directly to the internet.** Put it behind a reverse proxy that terminates TLS and performs its own authentication.
|
||||||
|
- **Keep the command runner disabled.** It is off by default, so leave it off. See [#5199](https://github.com/filebrowser/filebrowser/issues/5199) and [`docs/command-execution.md`](docs/command-execution.md).
|
||||||
|
- **Run it unprivileged, inside a container**, with only the directory you intend to serve mounted into it.
|
||||||
|
|
||||||
## Usage
|
## Documentation
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz/usage](https://github.com/filebrowser/docs/tree/master/usage).
|
Documentation on how to install, configure, and build this project lives in [`docs`](docs) in this repository.
|
||||||
|
|
||||||
## Contributing
|
[CONTRIBUTING.md](CONTRIBUTING.md) documents how to build and develop the project, which remains useful to anyone forking it.
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz/contributing](https://github.com/filebrowser/docs/tree/master/contributing).
|
## License
|
||||||
|
|
||||||
|
[Apache License 2.0](LICENSE) © File Browser Contributors
|
||||||
|
|
|
||||||
30
SECURITY.md
Normal file
30
SECURITY.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
No version receives security fixes. The last planned release has already shipped and no further changes will be merged.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | --------- |
|
||||||
|
| 2.x | ❌ |
|
||||||
|
| < 2.0 | ❌ |
|
||||||
|
|
||||||
|
## Before Reporting
|
||||||
|
|
||||||
|
This project is being wound down. To avoid duplicates, first check the [existing advisories](https://github.com/filebrowser/filebrowser/security/advisories) and open issues, and confirm:
|
||||||
|
|
||||||
|
- **It concerns this project, not a fork.** Reports about code, features, or endpoints that don't exist here belong to the relevant fork.
|
||||||
|
- **It isn't an already-known class** that remains unaddressed. Those are listed under [Security](README.md#security) in the README; reports covering them are likely to be closed as duplicates.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Until 2026-09-01, report privately via the [Security](https://github.com/filebrowser/filebrowser/security) page. After that date this repository is read-only and reports can no longer be submitted.
|
||||||
|
|
||||||
|
Please include, where possible:
|
||||||
|
|
||||||
|
- The commit the issue was found at
|
||||||
|
- A plaintext proof of concept (no binaries)
|
||||||
|
- Steps to reproduce
|
||||||
|
- Recommended remediation, if any
|
||||||
|
|
||||||
|
No fix will ship for any report: the last planned release has already shipped and no further changes will be merged. Reports may still be published as advisories so that people running File Browser can assess their exposure.
|
||||||
57
Taskfile.yml
Normal file
57
Taskfile.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:frontend:
|
||||||
|
desc: Build frontend assets
|
||||||
|
dir: frontend
|
||||||
|
cmds:
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm run build
|
||||||
|
|
||||||
|
build:backend:
|
||||||
|
desc: Build backend binary
|
||||||
|
cmds:
|
||||||
|
- go build -ldflags='-s -w -X "github.com/filebrowser/filebrowser/v2/version.Version={{.VERSION}}" -X "github.com/filebrowser/filebrowser/v2/version.CommitSHA={{.GIT_COMMIT}}"' -o filebrowser .
|
||||||
|
vars:
|
||||||
|
GIT_COMMIT:
|
||||||
|
sh: git log -n 1 --format=%h
|
||||||
|
VERSION:
|
||||||
|
sh: git describe --tags --abbrev=0 --match=v* | cut -c 2-
|
||||||
|
|
||||||
|
build:
|
||||||
|
desc: Build both frontend and backend
|
||||||
|
cmds:
|
||||||
|
- task: build:frontend
|
||||||
|
- task: build:backend
|
||||||
|
|
||||||
|
release:make:
|
||||||
|
internal: true
|
||||||
|
prompt: Do you wish to proceed?
|
||||||
|
cmds:
|
||||||
|
- pnpm dlx commit-and-tag-version -s
|
||||||
|
|
||||||
|
release:dry-run:
|
||||||
|
internal: true
|
||||||
|
cmds:
|
||||||
|
- pnpm dlx commit-and-tag-version --dry-run --skip
|
||||||
|
|
||||||
|
release:
|
||||||
|
desc: Create a new release
|
||||||
|
cmds:
|
||||||
|
- task: docs:cli:generate
|
||||||
|
- git add docs/cli
|
||||||
|
- |
|
||||||
|
if [[ `git status docs/cli --porcelain` ]]; then
|
||||||
|
git commit -m 'chore(docs): update CLI documentation'
|
||||||
|
fi
|
||||||
|
- task: release:dry-run
|
||||||
|
- task: release:make
|
||||||
|
|
||||||
|
docs:cli:generate:
|
||||||
|
cmds:
|
||||||
|
- rm -rf docs/cli
|
||||||
|
- mkdir -p docs/cli
|
||||||
|
- go run . docs
|
||||||
|
generates:
|
||||||
|
- docs/cli
|
||||||
|
|
||||||
|
|
@ -3,13 +3,14 @@ package auth
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Auther is the authentication interface.
|
// Auther is the authentication interface.
|
||||||
type Auther interface {
|
type Auther interface {
|
||||||
// Auth is called to authenticate a request.
|
// Auth is called to authenticate a request.
|
||||||
Auth(r *http.Request, s *users.Storage, root string) (*users.User, error)
|
Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error)
|
||||||
// LoginPage indicates if this auther needs a login page.
|
// LoginPage indicates if this auther needs a login page.
|
||||||
LoginPage() bool
|
LoginPage() bool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
288
auth/hook.go
Normal file
288
auth/hook.go
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MethodHookAuth is used to identify hook auth.
|
||||||
|
const MethodHookAuth settings.AuthMethod = "hook"
|
||||||
|
|
||||||
|
type hookCred struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HookAuth is a hook implementation of an Auther.
|
||||||
|
type HookAuth struct {
|
||||||
|
Users users.Store `json:"-"`
|
||||||
|
Settings *settings.Settings `json:"-"`
|
||||||
|
Server *settings.Server `json:"-"`
|
||||||
|
Cred hookCred `json:"-"`
|
||||||
|
Fields hookFields `json:"-"`
|
||||||
|
Command string `json:"command"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth authenticates the user via a json in content body.
|
||||||
|
func (a *HookAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
|
var cred hookCred
|
||||||
|
|
||||||
|
if r.Body == nil {
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Users = usr
|
||||||
|
a.Settings = stg
|
||||||
|
a.Server = srv
|
||||||
|
a.Cred = cred
|
||||||
|
|
||||||
|
action, err := a.RunCommand()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "auth":
|
||||||
|
u, err := a.SaveUser()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
case "block":
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
case "pass":
|
||||||
|
u, err := a.Users.Get(a.Server.Root, a.Server.FollowExternalSymlinks, a.Cred.Username)
|
||||||
|
if err != nil || !users.CheckPwd(a.Cred.Password, u.Password) {
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid hook action: %s", action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginPage tells that hook auth requires a login page.
|
||||||
|
func (a *HookAuth) LoginPage() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommand starts the hook command and returns the action
|
||||||
|
func (a *HookAuth) RunCommand() (string, error) {
|
||||||
|
command := strings.Split(a.Command, " ")
|
||||||
|
|
||||||
|
cmd := exec.Command(command[0], command[1:]...)
|
||||||
|
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.GetValues(string(out))
|
||||||
|
|
||||||
|
return a.Fields.Values["hook.action"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues creates a map with values from the key-value format string
|
||||||
|
func (a *HookAuth) GetValues(s string) {
|
||||||
|
m := map[string]string{}
|
||||||
|
|
||||||
|
// make line breaks consistent on Windows platform
|
||||||
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||||
|
|
||||||
|
// iterate input lines
|
||||||
|
for val := range strings.Lines(s) {
|
||||||
|
v := strings.SplitN(val, "=", 2)
|
||||||
|
|
||||||
|
// skips non key and value format
|
||||||
|
if len(v) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldKey := strings.TrimSpace(v[0])
|
||||||
|
fieldValue := strings.TrimSpace(v[1])
|
||||||
|
|
||||||
|
if a.Fields.IsValid(fieldKey) {
|
||||||
|
m[fieldKey] = fieldValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Fields.Values = m
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUser updates the existing user or creates a new one when not found
|
||||||
|
func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||||
|
u, err := a.Users.Get(a.Server.Root, a.Server.FollowExternalSymlinks, a.Cred.Username)
|
||||||
|
if err != nil && !errors.Is(err, fberrors.ErrNotExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u == nil {
|
||||||
|
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create user with the provided credentials
|
||||||
|
d := &users.User{
|
||||||
|
Username: a.Cred.Username,
|
||||||
|
Password: pass,
|
||||||
|
Scope: a.Settings.Defaults.Scope,
|
||||||
|
Locale: a.Settings.Defaults.Locale,
|
||||||
|
ViewMode: a.Settings.Defaults.ViewMode,
|
||||||
|
SingleClick: a.Settings.Defaults.SingleClick,
|
||||||
|
RedirectAfterCopyMove: a.Settings.Defaults.RedirectAfterCopyMove,
|
||||||
|
Sorting: a.Settings.Defaults.Sorting,
|
||||||
|
Perm: a.Settings.Defaults.Perm,
|
||||||
|
Commands: a.Settings.Defaults.Commands,
|
||||||
|
DateFormat: a.Settings.Defaults.DateFormat,
|
||||||
|
HideDotfiles: a.Settings.Defaults.HideDotfiles,
|
||||||
|
}
|
||||||
|
u = a.GetUser(d)
|
||||||
|
|
||||||
|
// A scope explicitly returned by the hook takes precedence over the
|
||||||
|
// automatic per-user home directory derivation.
|
||||||
|
_, explicitScope := a.Fields.Values["user.scope"]
|
||||||
|
derivedScope, err := a.Settings.CreateUserHome(u, a.Server.Root, explicitScope)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Printf("user: %s, home dir: [%s].", u.Username, u.Scope)
|
||||||
|
|
||||||
|
if err := a.Users.SaveProvisioned(u, derivedScope); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
|
||||||
|
u = a.GetUser(u)
|
||||||
|
|
||||||
|
// update the password when it doesn't match the current
|
||||||
|
if p {
|
||||||
|
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u.Password = pass
|
||||||
|
}
|
||||||
|
|
||||||
|
// update user with provided fields
|
||||||
|
err := a.Users.Update(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser returns a User filled with hook values or provided defaults
|
||||||
|
func (a *HookAuth) GetUser(d *users.User) *users.User {
|
||||||
|
// adds all permissions when user is admin
|
||||||
|
isAdmin := a.Fields.GetBoolean("user.perm.admin", d.Perm.Admin)
|
||||||
|
perms := users.Permissions{
|
||||||
|
Admin: isAdmin,
|
||||||
|
Execute: isAdmin || a.Fields.GetBoolean("user.perm.execute", d.Perm.Execute),
|
||||||
|
Create: isAdmin || a.Fields.GetBoolean("user.perm.create", d.Perm.Create),
|
||||||
|
Rename: isAdmin || a.Fields.GetBoolean("user.perm.rename", d.Perm.Rename),
|
||||||
|
Modify: isAdmin || a.Fields.GetBoolean("user.perm.modify", d.Perm.Modify),
|
||||||
|
Delete: isAdmin || a.Fields.GetBoolean("user.perm.delete", d.Perm.Delete),
|
||||||
|
Share: isAdmin || a.Fields.GetBoolean("user.perm.share", d.Perm.Share),
|
||||||
|
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
|
||||||
|
}
|
||||||
|
user := users.User{
|
||||||
|
ID: d.ID,
|
||||||
|
Username: d.Username,
|
||||||
|
Password: d.Password,
|
||||||
|
Scope: a.Fields.GetString("user.scope", d.Scope),
|
||||||
|
Locale: a.Fields.GetString("user.locale", d.Locale),
|
||||||
|
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
|
||||||
|
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
|
||||||
|
RedirectAfterCopyMove: a.Fields.GetBoolean("user.redirectAfterCopyMove", d.RedirectAfterCopyMove),
|
||||||
|
Sorting: files.Sorting{
|
||||||
|
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
|
||||||
|
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
|
||||||
|
},
|
||||||
|
Commands: a.Fields.GetArray("user.commands", d.Commands),
|
||||||
|
DateFormat: a.Fields.GetBoolean("user.dateFormat", d.DateFormat),
|
||||||
|
HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
|
||||||
|
Perm: perms,
|
||||||
|
LockPassword: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user
|
||||||
|
}
|
||||||
|
|
||||||
|
// hookFields is used to access fields from the hook
|
||||||
|
type hookFields struct {
|
||||||
|
Values map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// validHookFields contains names of the fields that can be used
|
||||||
|
var validHookFields = []string{
|
||||||
|
"hook.action",
|
||||||
|
"user.scope",
|
||||||
|
"user.locale",
|
||||||
|
"user.viewMode",
|
||||||
|
"user.singleClick",
|
||||||
|
"user.redirectAfterCopyMove",
|
||||||
|
"user.sorting.by",
|
||||||
|
"user.sorting.asc",
|
||||||
|
"user.commands",
|
||||||
|
"user.hideDotfiles",
|
||||||
|
"user.perm.admin",
|
||||||
|
"user.perm.execute",
|
||||||
|
"user.perm.create",
|
||||||
|
"user.perm.rename",
|
||||||
|
"user.perm.modify",
|
||||||
|
"user.perm.delete",
|
||||||
|
"user.perm.share",
|
||||||
|
"user.perm.download",
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid checks if the provided field is on the valid fields list
|
||||||
|
func (hf *hookFields) IsValid(field string) bool {
|
||||||
|
return slices.Contains(validHookFields, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString returns the string value or provided default
|
||||||
|
func (hf *hookFields) GetString(k, dv string) string {
|
||||||
|
val, ok := hf.Values[k]
|
||||||
|
if ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return dv
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoolean returns the bool value or provided default
|
||||||
|
func (hf *hookFields) GetBoolean(k string, dv bool) bool {
|
||||||
|
val, ok := hf.Values[k]
|
||||||
|
if ok {
|
||||||
|
return val == "true"
|
||||||
|
}
|
||||||
|
return dv
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArray returns the array value or provided default
|
||||||
|
func (hf *hookFields) GetArray(k string, dv []string) []string {
|
||||||
|
val, ok := hf.Values[k]
|
||||||
|
if ok && strings.TrimSpace(val) != "" {
|
||||||
|
return strings.Split(val, " ")
|
||||||
|
}
|
||||||
|
return dv
|
||||||
|
}
|
||||||
156
auth/hook_test.go
Normal file
156
auth/hook_test.go
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
// writeHookScript writes a POSIX shell script to a temp file and returns its
|
||||||
|
// path, marking it executable.
|
||||||
|
func writeHookScript(t *testing.T, body string) string {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(t.TempDir(), "hook.sh")
|
||||||
|
if err := os.WriteFile(path, []byte("#!/bin/sh\n"+body), 0o700); err != nil {
|
||||||
|
t.Fatalf("failed to write hook script: %v", err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunCommandNoCredentialInjection ensures that attacker-controlled
|
||||||
|
// credentials submitted at the unauthenticated login endpoint cannot be
|
||||||
|
// injected into the hook command string. Credentials must only ever reach the
|
||||||
|
// hook through the USERNAME/PASSWORD environment variables, never via string
|
||||||
|
// substitution into the command itself (CWE-78/CWE-88).
|
||||||
|
func TestRunCommandNoCredentialInjection(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("uses POSIX shell")
|
||||||
|
}
|
||||||
|
|
||||||
|
marker := filepath.Join(t.TempDir(), "pwned")
|
||||||
|
|
||||||
|
// The hook simply blocks. If the credential were ever interpolated into the
|
||||||
|
// command string and evaluated by a shell, the embedded `touch` would
|
||||||
|
// create the marker file.
|
||||||
|
script := writeHookScript(t, "echo hook.action=block\n")
|
||||||
|
|
||||||
|
a := &HookAuth{
|
||||||
|
Command: script,
|
||||||
|
Cred: hookCred{
|
||||||
|
Username: `"; touch ` + marker + `; #`,
|
||||||
|
Password: `$(touch ` + marker + `)`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
action, err := a.RunCommand()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RunCommand returned error: %v", err)
|
||||||
|
}
|
||||||
|
if action != "block" {
|
||||||
|
t.Fatalf("expected action %q, got %q", "block", action)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(marker); err == nil {
|
||||||
|
t.Fatalf("credential injection executed: marker file %q was created", marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunCommandReceivesCredentialsViaEnv verifies the supported contract: the
|
||||||
|
// hook receives credentials through the USERNAME and PASSWORD environment
|
||||||
|
// variables.
|
||||||
|
func TestRunCommandReceivesCredentialsViaEnv(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("uses POSIX shell")
|
||||||
|
}
|
||||||
|
|
||||||
|
script := writeHookScript(t, `if [ "$USERNAME" = alice ] && [ "$PASSWORD" = secret ]; then
|
||||||
|
echo hook.action=auth
|
||||||
|
else
|
||||||
|
echo hook.action=block
|
||||||
|
fi
|
||||||
|
`)
|
||||||
|
|
||||||
|
a := &HookAuth{
|
||||||
|
Command: script,
|
||||||
|
Cred: hookCred{
|
||||||
|
Username: "alice",
|
||||||
|
Password: "secret",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
action, err := a.RunCommand()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RunCommand returned error: %v", err)
|
||||||
|
}
|
||||||
|
if action != "auth" {
|
||||||
|
t.Fatalf("expected action %q, got %q", "auth", action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newHookAuth builds a HookAuth for a freshly provisioned user with the given
|
||||||
|
// parsed hook fields (hook.action=auth is added automatically).
|
||||||
|
func newHookAuth(store *mockUserStore, s *settings.Settings, srv *settings.Server, username string, fields map[string]string) *HookAuth {
|
||||||
|
fields["hook.action"] = "auth"
|
||||||
|
return &HookAuth{
|
||||||
|
Users: store,
|
||||||
|
Settings: s,
|
||||||
|
Server: srv,
|
||||||
|
Cred: hookCred{Username: username, Password: "a-strong-password"},
|
||||||
|
Fields: hookFields{Values: fields},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With CreateUserDir enabled and no explicit scope from the hook, a provisioned
|
||||||
|
// hook user must receive its own home directory rather than the server root.
|
||||||
|
func TestHookSaveUserCreateUserDirIsolatesScope(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
store := &mockUserStore{users: make(map[string]*users.User)}
|
||||||
|
srv := &settings.Server{Root: t.TempDir()}
|
||||||
|
s := &settings.Settings{
|
||||||
|
Key: []byte("key"),
|
||||||
|
CreateUserDir: true,
|
||||||
|
UserHomeBasePath: "/users",
|
||||||
|
Defaults: settings.UserDefaults{
|
||||||
|
Scope: ".",
|
||||||
|
Perm: users.Permissions{Create: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := newHookAuth(store, s, srv, "alice", map[string]string{}).SaveUser()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveUser error: %v", err)
|
||||||
|
}
|
||||||
|
if u.Scope != "/users/alice" {
|
||||||
|
t.Errorf("hook user without explicit scope: expected /users/alice, got %q", u.Scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A scope explicitly returned by the hook takes precedence over the automatic
|
||||||
|
// per-user home directory derivation.
|
||||||
|
func TestHookSaveUserRespectsExplicitScope(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
store := &mockUserStore{users: make(map[string]*users.User)}
|
||||||
|
srv := &settings.Server{Root: t.TempDir()}
|
||||||
|
s := &settings.Settings{
|
||||||
|
Key: []byte("key"),
|
||||||
|
CreateUserDir: true,
|
||||||
|
UserHomeBasePath: "/users",
|
||||||
|
Defaults: settings.UserDefaults{
|
||||||
|
Scope: ".",
|
||||||
|
Perm: users.Permissions{Create: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := newHookAuth(store, s, srv, "teamlead", map[string]string{"user.scope": "/shared/team"}).SaveUser()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveUser error: %v", err)
|
||||||
|
}
|
||||||
|
if u.Scope != "/shared/team" {
|
||||||
|
t.Errorf("explicit hook scope should win, got %q", u.Scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
auth/json.go
24
auth/json.go
|
|
@ -14,6 +14,10 @@ import (
|
||||||
// MethodJSONAuth is used to identify json auth.
|
// MethodJSONAuth is used to identify json auth.
|
||||||
const MethodJSONAuth settings.AuthMethod = "json"
|
const MethodJSONAuth settings.AuthMethod = "json"
|
||||||
|
|
||||||
|
// dummyHash is used to prevent user enumeration timing attacks.
|
||||||
|
// It MUST be a valid bcrypt hash.
|
||||||
|
const dummyHash = "$2a$10$O4mEMeOL/nit6zqe.WQXauLRbRlzb3IgLHsa26Pf0N/GiU9b.wK1m"
|
||||||
|
|
||||||
type jsonCred struct {
|
type jsonCred struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
|
@ -26,7 +30,7 @@ type JSONAuth struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth authenticates the user via a json in content body.
|
// Auth authenticates the user via a json in content body.
|
||||||
func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
|
func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
var cred jsonCred
|
var cred jsonCred
|
||||||
|
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
|
|
@ -39,8 +43,8 @@ func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users
|
||||||
}
|
}
|
||||||
|
|
||||||
// If ReCaptcha is enabled, check the code.
|
// If ReCaptcha is enabled, check the code.
|
||||||
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
|
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
|
||||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:shadow
|
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -51,8 +55,18 @@ func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := sto.Get(root, cred.Username)
|
u, err := usr.Get(srv.Root, srv.FollowExternalSymlinks, cred.Username)
|
||||||
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
|
|
||||||
|
hash := dummyHash
|
||||||
|
if err == nil {
|
||||||
|
hash = u.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
if !users.CheckPwd(cred.Password, hash) {
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
return nil, os.ErrPermission
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ const MethodNoAuth settings.AuthMethod = "noauth"
|
||||||
type NoAuth struct{}
|
type NoAuth struct{}
|
||||||
|
|
||||||
// Auth uses authenticates user 1.
|
// Auth uses authenticates user 1.
|
||||||
func (a NoAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
|
func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
return sto.Get(root, uint(1))
|
return usr.Get(srv.Root, srv.FollowExternalSymlinks, uint(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginPage tells that no auth doesn't require a login page.
|
// LoginPage tells that no auth doesn't require a login page.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
@ -18,14 +18,48 @@ type ProxyAuth struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth authenticates the user via an HTTP header.
|
// Auth authenticates the user via an HTTP header.
|
||||||
func (a ProxyAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
|
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
username := r.Header.Get(a.Header)
|
username := r.Header.Get(a.Header)
|
||||||
user, err := sto.Get(root, username)
|
user, err := usr.Get(srv.Root, srv.FollowExternalSymlinks, username)
|
||||||
if err == errors.ErrNotExist {
|
if errors.Is(err, fberrors.ErrNotExist) {
|
||||||
return nil, os.ErrPermission
|
return a.createUser(usr, setting, srv, username)
|
||||||
|
}
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
|
||||||
|
const randomPasswordLength = settings.DefaultMinimumPasswordLength + 10
|
||||||
|
pwd, err := users.RandomPwd(randomPasswordLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, err
|
var hashedRandomPassword string
|
||||||
|
hashedRandomPassword, err = users.ValidateAndHashPwd(pwd, setting.MinimumPasswordLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &users.User{
|
||||||
|
Username: username,
|
||||||
|
Password: hashedRandomPassword,
|
||||||
|
LockPassword: true,
|
||||||
|
}
|
||||||
|
setting.Defaults.Apply(user)
|
||||||
|
user.Perm.Admin = false
|
||||||
|
user.Perm.Execute = false
|
||||||
|
user.Commands = []string{}
|
||||||
|
|
||||||
|
var derivedScope bool
|
||||||
|
if derivedScope, err = setting.CreateUserHome(user, srv.Root, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = usr.SaveProvisioned(user, derivedScope); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginPage tells that proxy auth doesn't require a login page.
|
// LoginPage tells that proxy auth doesn't require a login page.
|
||||||
|
|
|
||||||
142
auth/proxy_test.go
Normal file
142
auth/proxy_test.go
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockUserStore struct {
|
||||||
|
users map[string]*users.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserStore) Get(_ string, _ bool, id interface{}) (*users.User, error) {
|
||||||
|
if v, ok := id.(string); ok {
|
||||||
|
if u, ok := m.users[v]; ok {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fberrors.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserStore) GetByScope(scope string) (*users.User, error) {
|
||||||
|
for _, u := range m.users {
|
||||||
|
if strings.EqualFold(u.Scope, scope) {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fberrors.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserStore) Gets(_ string, _ bool) ([]*users.User, error) { return nil, nil }
|
||||||
|
func (m *mockUserStore) Update(_ *users.User, _ ...string) error { return nil }
|
||||||
|
func (m *mockUserStore) Save(user *users.User) error {
|
||||||
|
m.users[user.Username] = user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserStore) SaveProvisioned(user *users.User, derivedScope bool) error {
|
||||||
|
if derivedScope {
|
||||||
|
if _, err := m.GetByScope(user.Scope); err == nil {
|
||||||
|
return fberrors.ErrExist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.Save(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserStore) Delete(_ interface{}) error { return nil }
|
||||||
|
func (m *mockUserStore) LastUpdate(_ uint) int64 { return 0 }
|
||||||
|
|
||||||
|
func TestProxyAuthCreateUserRestrictsDefaults(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
store := &mockUserStore{users: make(map[string]*users.User)}
|
||||||
|
srv := &settings.Server{Root: t.TempDir()}
|
||||||
|
|
||||||
|
s := &settings.Settings{
|
||||||
|
Key: []byte("key"),
|
||||||
|
AuthMethod: MethodProxyAuth,
|
||||||
|
Defaults: settings.UserDefaults{
|
||||||
|
Perm: users.Permissions{
|
||||||
|
Admin: true,
|
||||||
|
Execute: true,
|
||||||
|
Create: true,
|
||||||
|
Rename: true,
|
||||||
|
Modify: true,
|
||||||
|
Delete: true,
|
||||||
|
Share: true,
|
||||||
|
Download: true,
|
||||||
|
},
|
||||||
|
Commands: []string{"git", "ls", "cat", "id"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := ProxyAuth{Header: "X-Remote-User"}
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "/", http.NoBody)
|
||||||
|
req.Header.Set("X-Remote-User", "newproxyuser")
|
||||||
|
|
||||||
|
user, err := auth.Auth(req, store, s, srv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Auth() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Perm.Admin {
|
||||||
|
t.Error("auto-provisioned proxy user should not have Admin permission")
|
||||||
|
}
|
||||||
|
if user.Perm.Execute {
|
||||||
|
t.Error("auto-provisioned proxy user should not have Execute permission")
|
||||||
|
}
|
||||||
|
if len(user.Commands) != 0 {
|
||||||
|
t.Errorf("auto-provisioned proxy user should have empty Commands, got %v", user.Commands)
|
||||||
|
}
|
||||||
|
if !user.Perm.Create {
|
||||||
|
t.Error("auto-provisioned proxy user should retain Create permission from defaults")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With CreateUserDir enabled, two distinct proxy-authenticated users must each
|
||||||
|
// receive their own home directory instead of both inheriting the server root.
|
||||||
|
func TestProxyAuthCreateUserDirIsolatesScope(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
store := &mockUserStore{users: make(map[string]*users.User)}
|
||||||
|
srv := &settings.Server{Root: t.TempDir()}
|
||||||
|
s := &settings.Settings{
|
||||||
|
Key: []byte("key"),
|
||||||
|
AuthMethod: MethodProxyAuth,
|
||||||
|
CreateUserDir: true,
|
||||||
|
UserHomeBasePath: "/users",
|
||||||
|
Defaults: settings.UserDefaults{
|
||||||
|
Scope: ".",
|
||||||
|
Perm: users.Permissions{Create: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := ProxyAuth{Header: "X-Remote-User"}
|
||||||
|
provision := func(name string) *users.User {
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "/", http.NoBody)
|
||||||
|
req.Header.Set("X-Remote-User", name)
|
||||||
|
u, err := auth.Auth(req, store, s, srv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Auth(%q) error: %v", name, err)
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
alice := provision("alice")
|
||||||
|
bob := provision("bob")
|
||||||
|
|
||||||
|
if alice.Scope == "/" || bob.Scope == "/" {
|
||||||
|
t.Fatalf("provisioned users inherited the server root: alice=%q bob=%q", alice.Scope, bob.Scope)
|
||||||
|
}
|
||||||
|
if alice.Scope == bob.Scope {
|
||||||
|
t.Fatalf("distinct users must get distinct scopes, both got %q", alice.Scope)
|
||||||
|
}
|
||||||
|
if alice.Scope != "/users/alice" {
|
||||||
|
t.Errorf("expected /users/alice, got %q", alice.Scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
branding/banner.png
Normal file
BIN
branding/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
1
branding/banner.svg
Normal file
1
branding/banner.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 10 KiB |
BIN
branding/icon.png
Normal file
BIN
branding/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
1
branding/icon.svg
Normal file
1
branding/icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="700" height="700" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"><defs><style>.prefix__fil1{fill:#fefefe}.prefix__fil6{fill:#006498}.prefix__fil5{fill:#bdeaff}</style></defs><g id="prefix__Layer_x0020_1"><path d="M80 0h540c44 0 80 36 80 80v540c0 44-36 80-80 80H80c-44 0-80-36-80-80V80C0 36 36 0 80 0z" fill="#455a64"/><path class="prefix__fil1" d="M350 71c154 0 279 125 279 279S504 629 350 629 71 504 71 350 196 71 350 71z"/><path d="M475 236l118 151c3 116-149 252-292 198l-76-99 114-156s138-95 136-94z" fill="#332c2b" fill-opacity=".149"/><path d="M231 211h208l38 24v246c0 5-3 8-8 8H231c-5 0-8-3-8-8V219c0-5 3-8 8-8z" fill="#2bbcff"/><path d="M231 211h208l38 24v2l-37-23H231c-4 0-7 3-7 7v263c-1-1-1-2-1-3V219c0-5 3-8 8-8z" fill="#53c6fc"/><path class="prefix__fil5" d="M305 212h113v98H305zM255 363h189c3 0 5 2 5 4v116H250V367c0-2 2-4 5-4z"/><path class="prefix__fil6" d="M250 470h199v13H250zM380 226h10c3 0 6 2 6 5v40c0 3-3 6-6 6h-10c-3 0-6-3-6-6v-40c0-3 3-5 6-5z"/><path class="prefix__fil1" d="M254 226c10 0 17 7 17 17 0 9-7 16-17 16-9 0-17-7-17-16 0-10 8-17 17-17z"/><path class="prefix__fil6" d="M267 448h165c2 0 3 1 3 3 0 1-1 3-3 3H267c-2 0-3-2-3-3 0-2 1-3 3-3zM267 415h165c2 0 3 1 3 3 0 1-1 2-3 2H267c-2 0-3-1-3-2 0-2 1-3 3-3zM267 381h165c2 0 3 2 3 3 0 2-1 3-3 3H267c-2 0-3-1-3-3 0-1 1-3 3-3z"/><path class="prefix__fil1" d="M236 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5zM463 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z"/><path class="prefix__fil6" d="M305 212h-21v98h21z"/><path d="M477 479v2c0 5-3 8-8 8H231c-5 0-8-3-8-8v-2c0 4 3 8 8 8h238c5 0 8-4 8-8z" fill="#0ea5eb"/><path d="M350 70c155 0 280 125 280 280S505 630 350 630 70 505 70 350 195 70 350 70zm0 46c129 0 234 105 234 234S479 584 350 584 116 479 116 350s105-234 234-234z" fill="#2979ff"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
branding/logo.png
Normal file
BIN
branding/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
1
branding/logo.svg
Normal file
1
branding/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="560" height="560" version="1.1" id="prefix__svg44" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision"><defs id="prefix__defs4"><style type="text/css" id="style2">.prefix__fil1{fill:#fefefe}.prefix__fil6{fill:#006498}.prefix__fil5{fill:#bdeaff}</style></defs><g id="prefix__g85" transform="translate(-70 -70)"><path class="prefix__fil1" d="M350 71c154 0 279 125 279 279S504 629 350 629 71 504 71 350 196 71 350 71z" id="prefix__path9" fill="#fefefe"/><path d="M475 236l118 151c3 116-149 252-292 198l-76-99 114-156s138-95 136-94z" id="prefix__path11" fill="#332c2b" fill-opacity=".149"/><path d="M231 211h208l38 24v246c0 5-3 8-8 8H231c-5 0-8-3-8-8V219c0-5 3-8 8-8z" id="prefix__path13" fill="#2bbcff"/><path d="M231 211h208l38 24v2l-37-23H231c-4 0-7 3-7 7v263c-1-1-1-2-1-3V219c0-5 3-8 8-8z" id="prefix__path15" fill="#53c6fc"/><path class="prefix__fil5" id="prefix__polygon17" fill="#bdeaff" d="M305 212h113v98H305z"/><path class="prefix__fil5" d="M255 363h189c3 0 5 2 5 4v116H250V367c0-2 2-4 5-4z" id="prefix__path19" fill="#bdeaff"/><path class="prefix__fil6" id="prefix__polygon21" fill="#006498" d="M250 470h199v13H250z"/><path class="prefix__fil6" d="M380 226h10c3 0 6 2 6 5v40c0 3-3 6-6 6h-10c-3 0-6-3-6-6v-40c0-3 3-5 6-5z" id="prefix__path23" fill="#006498"/><path class="prefix__fil1" d="M254 226c10 0 17 7 17 17 0 9-7 16-17 16-9 0-17-7-17-16 0-10 8-17 17-17z" id="prefix__path25" fill="#fefefe"/><path class="prefix__fil6" d="M267 448h165c2 0 3 1 3 3 0 1-1 3-3 3H267c-2 0-3-2-3-3 0-2 1-3 3-3z" id="prefix__path27" fill="#006498"/><path class="prefix__fil6" d="M267 415h165c2 0 3 1 3 3 0 1-1 2-3 2H267c-2 0-3-1-3-2 0-2 1-3 3-3z" id="prefix__path29" fill="#006498"/><path class="prefix__fil6" d="M267 381h165c2 0 3 2 3 3 0 2-1 3-3 3H267c-2 0-3-1-3-3 0-1 1-3 3-3z" id="prefix__path31" fill="#006498"/><path class="prefix__fil1" d="M236 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path33" fill="#fefefe"/><path class="prefix__fil1" d="M463 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path35" fill="#fefefe"/><path class="prefix__fil6" id="prefix__polygon37" fill="#006498" d="M305 212h-21v98h21z"/><path d="M477 479v2c0 5-3 8-8 8H231c-5 0-8-3-8-8v-2c0 4 3 8 8 8h238c5 0 8-4 8-8z" id="prefix__path39" fill="#0ea5eb"/><path d="M350 70c155 0 280 125 280 280S505 630 350 630 70 505 70 350 195 70 350 70zm0 46c129 0 234 105 234 234S479 584 350 584 116 479 116 350s105-234 234-234z" id="prefix__path41" fill="#2979ff"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
10
cmd/cmd.go
10
cmd/cmd.go
|
|
@ -1,12 +1,6 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Execute executes the commands.
|
// Execute executes the commands.
|
||||||
func Execute() {
|
func Execute() error {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
return rootCmd.Execute()
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
cmd/cmd_test.go
Normal file
61
cmd/cmd_test.go
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestEnvCollisions ensures that there are no collisions in the produced environment
|
||||||
|
// variable names for all commands and their flags.
|
||||||
|
func TestEnvCollisions(t *testing.T) {
|
||||||
|
testEnvCollisions(t, rootCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEnvCollisions(t *testing.T, cmd *cobra.Command) {
|
||||||
|
for _, cmd := range cmd.Commands() {
|
||||||
|
testEnvCollisions(t, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
replacements := generateEnvKeyReplacements(cmd)
|
||||||
|
envVariables := []string{}
|
||||||
|
|
||||||
|
for i := range replacements {
|
||||||
|
if i%2 != 0 {
|
||||||
|
envVariables = append(envVariables, replacements[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicates := lo.FindDuplicates(envVariables)
|
||||||
|
|
||||||
|
if len(duplicates) > 0 {
|
||||||
|
t.Errorf("Found duplicate environment variable keys for command %q: %v", cmd.Name(), duplicates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetSettingsFollowExternalSymlinks ensures that the followExternalSymlinks
|
||||||
|
// flag is persisted to the server config when set via "config set".
|
||||||
|
func TestGetSettingsFollowExternalSymlinks(t *testing.T) {
|
||||||
|
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||||
|
addConfigFlags(flags)
|
||||||
|
|
||||||
|
if err := flags.Parse([]string{"--followExternalSymlinks"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
set := &settings.Settings{AuthMethod: auth.MethodJSONAuth}
|
||||||
|
ser := &settings.Server{}
|
||||||
|
|
||||||
|
if _, err := getSettings(flags, set, ser, &auth.JSONAuth{}, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ser.FollowExternalSymlinks {
|
||||||
|
t.Error("expected FollowExternalSymlinks to be persisted as true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,14 +14,19 @@ var cmdsAddCmd = &cobra.Command{
|
||||||
Use: "add <event> <command>",
|
Use: "add <event> <command>",
|
||||||
Short: "Add a command to run on a specific event",
|
Short: "Add a command to run on a specific event",
|
||||||
Long: `Add a command to run on a specific event.`,
|
Long: `Add a command to run on a specific event.`,
|
||||||
Args: cobra.MinimumNArgs(2), //nolint:mnd
|
Args: cobra.MinimumNArgs(2),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := st.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
command := strings.Join(args[1:], " ")
|
command := strings.Join(args[1:], " ")
|
||||||
s.Commands[args[0]] = append(s.Commands[args[0]], command)
|
s.Commands[args[0]] = append(s.Commands[args[0]], command)
|
||||||
err = d.store.Settings.Save(s)
|
err = st.Settings.Save(s)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printEvents(s.Commands)
|
printEvents(s.Commands)
|
||||||
}, pythonConfig{}),
|
return nil
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,16 @@ var cmdsLsCmd = &cobra.Command{
|
||||||
Short: "List all commands for each event",
|
Short: "List all commands for each event",
|
||||||
Long: `List all commands for each event.`,
|
Long: `List all commands for each event.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := st.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
evt := mustGetString(cmd.Flags(), "event")
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
evt, err := cmd.Flags().GetString("event")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if evt == "" {
|
if evt == "" {
|
||||||
printEvents(s.Commands)
|
printEvents(s.Commands)
|
||||||
|
|
@ -27,5 +33,7 @@ var cmdsLsCmd = &cobra.Command{
|
||||||
show["after_"+evt] = s.Commands["after_"+evt]
|
show["after_"+evt] = s.Commands["after_"+evt]
|
||||||
printEvents(show)
|
printEvents(show)
|
||||||
}
|
}
|
||||||
}, pythonConfig{}),
|
|
||||||
|
return nil
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
|
||||||
you can remove all commands from 'index' to 'index_end',
|
you can remove all commands from 'index' to 'index_end',
|
||||||
including 'index_end'.`,
|
including 'index_end'.`,
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:mnd
|
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,22 +35,31 @@ including 'index_end'.`,
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := st.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
evt := args[0]
|
evt := args[0]
|
||||||
|
|
||||||
i, err := strconv.Atoi(args[1])
|
i, err := strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 3 { //nolint:mnd
|
if len(args) == 3 {
|
||||||
f, err = strconv.Atoi(args[2])
|
f, err = strconv.Atoi(args[2])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
|
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
|
||||||
err = d.store.Settings.Save(s)
|
err = st.Settings.Save(s)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printEvents(s.Commands)
|
printEvents(s.Commands)
|
||||||
}, pythonConfig{}),
|
return nil
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
354
cmd/config.go
354
cmd/config.go
|
|
@ -2,7 +2,7 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
nerrors "errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -30,24 +30,44 @@ var configCmd = &cobra.Command{
|
||||||
func addConfigFlags(flags *pflag.FlagSet) {
|
func addConfigFlags(flags *pflag.FlagSet) {
|
||||||
addServerFlags(flags)
|
addServerFlags(flags)
|
||||||
addUserFlags(flags)
|
addUserFlags(flags)
|
||||||
|
|
||||||
flags.BoolP("signup", "s", false, "allow users to signup")
|
flags.BoolP("signup", "s", false, "allow users to signup")
|
||||||
|
flags.Bool("hideLoginButton", false, "hide login button from public pages")
|
||||||
|
flags.Bool("createUserDir", false, "generate user's home directory automatically")
|
||||||
|
flags.Uint("minimumPasswordLength", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
|
||||||
flags.String("shell", "", "shell command to which other commands should be appended")
|
flags.String("shell", "", "shell command to which other commands should be appended")
|
||||||
|
|
||||||
|
// NB: these are string so they can be presented as octal in the help text
|
||||||
|
// as that's the conventional representation for modes in Unix.
|
||||||
|
flags.String("fileMode", fmt.Sprintf("%O", settings.DefaultFileMode), "mode bits that new files are created with")
|
||||||
|
flags.String("dirMode", fmt.Sprintf("%O", settings.DefaultDirMode), "mode bits that new directories are created with")
|
||||||
|
|
||||||
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
|
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
|
||||||
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
|
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
|
||||||
|
flags.String("auth.command", "", "command for auth.method=hook")
|
||||||
|
flags.String("auth.logoutPage", "", "url of custom logout page")
|
||||||
|
|
||||||
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
|
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
|
||||||
flags.String("recaptcha.key", "", "ReCaptcha site key")
|
flags.String("recaptcha.key", "", "ReCaptcha site key")
|
||||||
flags.String("recaptcha.secret", "", "ReCaptcha secret")
|
flags.String("recaptcha.secret", "", "ReCaptcha secret")
|
||||||
|
|
||||||
flags.String("branding.name", "", "replace 'File Browser' by this name")
|
flags.String("branding.name", "", "replace 'File Browser' by this name")
|
||||||
|
flags.String("branding.theme", "", "set the theme")
|
||||||
|
flags.String("branding.color", "", "set the theme color")
|
||||||
flags.String("branding.files", "", "path to directory with images and custom styles")
|
flags.String("branding.files", "", "path to directory with images and custom styles")
|
||||||
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
||||||
|
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
|
||||||
|
|
||||||
|
flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size")
|
||||||
|
flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count")
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
|
||||||
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
|
methodStr, err := flags.GetString("auth.method")
|
||||||
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
method := settings.AuthMethod(methodStr)
|
||||||
|
|
||||||
var defaultAuther map[string]interface{}
|
var defaultAuther map[string]interface{}
|
||||||
if len(defaults) > 0 {
|
if len(defaults) > 0 {
|
||||||
|
|
@ -58,79 +78,143 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
||||||
method = def.AuthMethod
|
method = def.AuthMethod
|
||||||
case auth.Auther:
|
case auth.Auther:
|
||||||
ms, err := json.Marshal(def)
|
ms, err := json.Marshal(def)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
err = json.Unmarshal(ms, &defaultAuther)
|
err = json.Unmarshal(ms, &defaultAuther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var auther auth.Auther
|
return method, defaultAuther, nil
|
||||||
if method == auth.MethodProxyAuth {
|
|
||||||
header := mustGetString(flags, "auth.header")
|
|
||||||
|
|
||||||
if header == "" {
|
|
||||||
header = defaultAuther["header"].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if header == "" {
|
|
||||||
checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'"))
|
|
||||||
}
|
|
||||||
|
|
||||||
auther = &auth.ProxyAuth{Header: header}
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == auth.MethodNoAuth {
|
|
||||||
auther = &auth.NoAuth{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == auth.MethodJSONAuth {
|
|
||||||
jsonAuth := &auth.JSONAuth{}
|
|
||||||
host := mustGetString(flags, "recaptcha.host")
|
|
||||||
key := mustGetString(flags, "recaptcha.key")
|
|
||||||
secret := mustGetString(flags, "recaptcha.secret")
|
|
||||||
|
|
||||||
if key == "" {
|
|
||||||
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
|
||||||
key = kmap["key"].(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if secret == "" {
|
|
||||||
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
|
||||||
secret = smap["secret"].(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if key != "" && secret != "" {
|
|
||||||
jsonAuth.ReCaptcha = &auth.ReCaptcha{
|
|
||||||
Host: host,
|
|
||||||
Key: key,
|
|
||||||
Secret: secret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auther = jsonAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
if auther == nil {
|
|
||||||
panic(errors.ErrInvalidAuthMethod)
|
|
||||||
}
|
|
||||||
|
|
||||||
return method, auther
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
|
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
|
||||||
|
header, err := flags.GetString("auth.header")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if header == "" && defaultAuther != nil {
|
||||||
|
header = defaultAuther["header"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if header == "" {
|
||||||
|
return nil, errors.New("you must set the flag 'auth.header' for method 'proxy'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &auth.ProxyAuth{Header: header}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNoAuth() auth.Auther {
|
||||||
|
return &auth.NoAuth{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
|
||||||
|
jsonAuth := &auth.JSONAuth{}
|
||||||
|
host, err := flags.GetString("recaptcha.host")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := flags.GetString("recaptcha.key")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := flags.GetString("recaptcha.secret")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == "" {
|
||||||
|
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
||||||
|
key = kmap["key"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret == "" {
|
||||||
|
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
||||||
|
secret = smap["secret"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key != "" && secret != "" {
|
||||||
|
jsonAuth.ReCaptcha = &auth.ReCaptcha{
|
||||||
|
Host: host,
|
||||||
|
Key: key,
|
||||||
|
Secret: secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonAuth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
|
||||||
|
command, err := flags.GetString("auth.command")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if command == "" {
|
||||||
|
command = defaultAuther["command"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
return nil, errors.New("you must set the flag 'auth.command' for method 'hook'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &auth.HookAuth{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther, error) {
|
||||||
|
method, defaultAuther, err := getAuthMethod(flags, defaults...)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var auther auth.Auther
|
||||||
|
switch method {
|
||||||
|
case auth.MethodProxyAuth:
|
||||||
|
auther, err = getProxyAuth(flags, defaultAuther)
|
||||||
|
case auth.MethodNoAuth:
|
||||||
|
auther = getNoAuth()
|
||||||
|
case auth.MethodJSONAuth:
|
||||||
|
auther, err = getJSONAuth(flags, defaultAuther)
|
||||||
|
case auth.MethodHookAuth:
|
||||||
|
auther, err = getHookAuth(flags, defaultAuther)
|
||||||
|
default:
|
||||||
|
return "", nil, fberrors.ErrInvalidAuthMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return method, auther, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) error {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
|
||||||
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
|
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
|
||||||
|
fmt.Fprintf(w, "Hide Login Button:\t%t\n", set.HideLoginButton)
|
||||||
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
|
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
|
||||||
fmt.Fprintf(w, "Auth method:\t%s\n", set.AuthMethod)
|
fmt.Fprintf(w, "Logout Page:\t%s\n", set.LogoutPage)
|
||||||
|
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength)
|
||||||
|
fmt.Fprintf(w, "Auth Method:\t%s\n", set.AuthMethod)
|
||||||
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
|
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
|
||||||
|
|
||||||
fmt.Fprintln(w, "\nBranding:")
|
fmt.Fprintln(w, "\nBranding:")
|
||||||
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
|
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
|
||||||
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
|
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
|
||||||
fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal)
|
fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal)
|
||||||
|
fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
|
||||||
|
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
|
||||||
|
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
|
||||||
|
|
||||||
fmt.Fprintln(w, "\nServer:")
|
fmt.Fprintln(w, "\nServer:")
|
||||||
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
|
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
|
||||||
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
|
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
|
||||||
|
|
@ -140,14 +224,34 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
||||||
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
|
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
|
||||||
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
|
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
|
||||||
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
|
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
|
||||||
|
fmt.Fprintf(w, "\tToken Expiration Time:\t%s\n", ser.TokenExpirationTime)
|
||||||
|
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
|
||||||
|
fmt.Fprintf(w, "\tThumbnails Enabled:\t%t\n", ser.EnableThumbnails)
|
||||||
|
fmt.Fprintf(w, "\tResize Preview:\t%t\n", ser.ResizePreview)
|
||||||
|
fmt.Fprintf(w, "\tType Detection by Header:\t%t\n", ser.TypeDetectionByHeader)
|
||||||
|
fmt.Fprintf(w, "\tFollow External Symlinks:\t%t\n", ser.FollowExternalSymlinks)
|
||||||
|
|
||||||
|
fmt.Fprintln(w, "\nTUS:")
|
||||||
|
fmt.Fprintf(w, "\tChunk size:\t%d\n", set.Tus.ChunkSize)
|
||||||
|
fmt.Fprintf(w, "\tRetry count:\t%d\n", set.Tus.RetryCount)
|
||||||
|
|
||||||
fmt.Fprintln(w, "\nDefaults:")
|
fmt.Fprintln(w, "\nDefaults:")
|
||||||
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
|
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
|
||||||
|
fmt.Fprintf(w, "\tDateFormat:\t%t\n", set.Defaults.DateFormat)
|
||||||
|
fmt.Fprintf(w, "\tHideDotfiles:\t%t\n", set.Defaults.HideDotfiles)
|
||||||
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
|
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
|
||||||
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
|
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
|
||||||
|
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
|
||||||
|
fmt.Fprintf(w, "\tRedirect after Copy/Move:\t%t\n", set.Defaults.RedirectAfterCopyMove)
|
||||||
|
fmt.Fprintf(w, "\tFile Creation Mode:\t%O\n", set.FileMode)
|
||||||
|
fmt.Fprintf(w, "\tDirectory Creation Mode:\t%O\n", set.DirMode)
|
||||||
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
|
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
|
||||||
|
fmt.Fprintf(w, "\tAce editor syntax highlighting theme:\t%s\n", set.Defaults.AceEditorTheme)
|
||||||
|
|
||||||
fmt.Fprintf(w, "\tSorting:\n")
|
fmt.Fprintf(w, "\tSorting:\n")
|
||||||
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
|
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
|
||||||
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
|
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
|
||||||
|
|
||||||
fmt.Fprintf(w, "\tPermissions:\n")
|
fmt.Fprintf(w, "\tPermissions:\n")
|
||||||
fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin)
|
fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin)
|
||||||
fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute)
|
fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute)
|
||||||
|
|
@ -157,9 +261,135 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
||||||
fmt.Fprintf(w, "\t\tDelete:\t%t\n", set.Defaults.Perm.Delete)
|
fmt.Fprintf(w, "\t\tDelete:\t%t\n", set.Defaults.Perm.Delete)
|
||||||
fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share)
|
fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share)
|
||||||
fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download)
|
fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download)
|
||||||
|
|
||||||
w.Flush()
|
w.Flush()
|
||||||
|
|
||||||
b, err := json.MarshalIndent(auther, "", " ")
|
b, err := json.MarshalIndent(auther, "", " ")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
|
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Server, auther auth.Auther, all bool) (auth.Auther, error) {
|
||||||
|
errs := []error{}
|
||||||
|
hasAuth := false
|
||||||
|
|
||||||
|
visit := func(flag *pflag.Flag) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch flag.Name {
|
||||||
|
// Server flags from [addServerFlags]
|
||||||
|
case "address":
|
||||||
|
ser.Address, err = flags.GetString(flag.Name)
|
||||||
|
case "log":
|
||||||
|
ser.Log, err = flags.GetString(flag.Name)
|
||||||
|
case "port":
|
||||||
|
ser.Port, err = flags.GetString(flag.Name)
|
||||||
|
case "cert":
|
||||||
|
ser.TLSCert, err = flags.GetString(flag.Name)
|
||||||
|
case "key":
|
||||||
|
ser.TLSKey, err = flags.GetString(flag.Name)
|
||||||
|
case "root":
|
||||||
|
ser.Root, err = flags.GetString(flag.Name)
|
||||||
|
case "socket":
|
||||||
|
ser.Socket, err = flags.GetString(flag.Name)
|
||||||
|
case "baseURL":
|
||||||
|
ser.BaseURL, err = flags.GetString(flag.Name)
|
||||||
|
case "tokenExpirationTime":
|
||||||
|
ser.TokenExpirationTime, err = flags.GetString(flag.Name)
|
||||||
|
case "disableThumbnails":
|
||||||
|
ser.EnableThumbnails, err = flags.GetBool(flag.Name)
|
||||||
|
ser.EnableThumbnails = !ser.EnableThumbnails
|
||||||
|
case "disablePreviewResize":
|
||||||
|
ser.ResizePreview, err = flags.GetBool(flag.Name)
|
||||||
|
ser.ResizePreview = !ser.ResizePreview
|
||||||
|
case "disableExec":
|
||||||
|
ser.EnableExec, err = flags.GetBool(flag.Name)
|
||||||
|
ser.EnableExec = !ser.EnableExec
|
||||||
|
case "disableTypeDetectionByHeader":
|
||||||
|
ser.TypeDetectionByHeader, err = flags.GetBool(flag.Name)
|
||||||
|
ser.TypeDetectionByHeader = !ser.TypeDetectionByHeader
|
||||||
|
case "disableImageResolutionCalc":
|
||||||
|
ser.ImageResolutionCal, err = flags.GetBool(flag.Name)
|
||||||
|
ser.ImageResolutionCal = !ser.ImageResolutionCal
|
||||||
|
case "followExternalSymlinks":
|
||||||
|
ser.FollowExternalSymlinks, err = flags.GetBool(flag.Name)
|
||||||
|
|
||||||
|
// Settings flags from [addConfigFlags]
|
||||||
|
case "signup":
|
||||||
|
set.Signup, err = flags.GetBool(flag.Name)
|
||||||
|
case "hideLoginButton":
|
||||||
|
set.HideLoginButton, err = flags.GetBool(flag.Name)
|
||||||
|
case "createUserDir":
|
||||||
|
set.CreateUserDir, err = flags.GetBool(flag.Name)
|
||||||
|
case "minimumPasswordLength":
|
||||||
|
set.MinimumPasswordLength, err = flags.GetUint(flag.Name)
|
||||||
|
case "shell":
|
||||||
|
var shell string
|
||||||
|
shell, err = flags.GetString(flag.Name)
|
||||||
|
if err == nil {
|
||||||
|
set.Shell = convertCmdStrToCmdArray(shell)
|
||||||
|
}
|
||||||
|
case "fileMode":
|
||||||
|
set.FileMode, err = getAndParseFileMode(flags, flag.Name)
|
||||||
|
case "dirMode":
|
||||||
|
set.DirMode, err = getAndParseFileMode(flags, flag.Name)
|
||||||
|
case "auth.method":
|
||||||
|
hasAuth = true
|
||||||
|
case "auth.logoutPage":
|
||||||
|
set.LogoutPage, err = flags.GetString(flag.Name)
|
||||||
|
case "branding.name":
|
||||||
|
set.Branding.Name, err = flags.GetString(flag.Name)
|
||||||
|
case "branding.theme":
|
||||||
|
set.Branding.Theme, err = flags.GetString(flag.Name)
|
||||||
|
case "branding.color":
|
||||||
|
set.Branding.Color, err = flags.GetString(flag.Name)
|
||||||
|
case "branding.files":
|
||||||
|
set.Branding.Files, err = flags.GetString(flag.Name)
|
||||||
|
case "branding.disableExternal":
|
||||||
|
set.Branding.DisableExternal, err = flags.GetBool(flag.Name)
|
||||||
|
case "branding.disableUsedPercentage":
|
||||||
|
set.Branding.DisableUsedPercentage, err = flags.GetBool(flag.Name)
|
||||||
|
case "tus.chunkSize":
|
||||||
|
set.Tus.ChunkSize, err = flags.GetUint64(flag.Name)
|
||||||
|
case "tus.retryCount":
|
||||||
|
set.Tus.RetryCount, err = flags.GetUint16(flag.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if all {
|
||||||
|
flags.VisitAll(visit)
|
||||||
|
} else {
|
||||||
|
flags.Visit(visit)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := errors.Join(errs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = getUserDefaults(flags, &set.Defaults, all)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if all {
|
||||||
|
set.AuthMethod, auther, err = getAuthentication(flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return auther, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,19 @@ var configCatCmd = &cobra.Command{
|
||||||
Short: "Prints the configuration",
|
Short: "Prints the configuration",
|
||||||
Long: `Prints the configuration.`,
|
Long: `Prints the configuration.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(_ *cobra.Command, _ []string, st *store) error {
|
||||||
set, err := d.store.Settings.Get()
|
set, err := st.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
ser, err := d.store.Settings.GetServer()
|
return err
|
||||||
checkErr(err)
|
}
|
||||||
auther, err := d.store.Auth.Get(set.AuthMethod)
|
ser, err := st.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
printSettings(ser, set, auther)
|
return err
|
||||||
}, pythonConfig{}),
|
}
|
||||||
|
auther, err := st.Auth.Get(set.AuthMethod)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return printSettings(ser, set, auther)
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,21 @@ var configExportCmd = &cobra.Command{
|
||||||
json or yaml file. This exported configuration can be changed,
|
json or yaml file. This exported configuration can be changed,
|
||||||
and imported again with 'config import' command.`,
|
and imported again with 'config import' command.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||||
settings, err := d.store.Settings.Get()
|
settings, err := st.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
server, err := d.store.Settings.GetServer()
|
server, err := st.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
auther, err := d.store.Auth.Get(settings.AuthMethod)
|
auther, err := st.Auth.Get(settings.AuthMethod)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
data := &settingsFile{
|
data := &settingsFile{
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
|
|
@ -32,6 +38,9 @@ and imported again with 'config import' command.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = marshal(args[0], data)
|
err = marshal(args[0], data)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
}, pythonConfig{}),
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
@ -34,59 +33,88 @@ database.
|
||||||
|
|
||||||
The path must be for a json or yaml file.`,
|
The path must be for a json or yaml file.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||||
var key []byte
|
var key []byte
|
||||||
if d.hadDB {
|
var err error
|
||||||
settings, err := d.store.Settings.Get()
|
if st.databaseExisted {
|
||||||
checkErr(err)
|
settings, settingErr := st.Settings.Get()
|
||||||
|
if settingErr != nil {
|
||||||
|
return settingErr
|
||||||
|
}
|
||||||
key = settings.Key
|
key = settings.Key
|
||||||
} else {
|
} else {
|
||||||
key = generateKey()
|
key = generateKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
file := settingsFile{}
|
file := settingsFile{}
|
||||||
err := unmarshal(args[0], &file)
|
err = unmarshal(args[0], &file)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.Settings == nil || file.Server == nil {
|
||||||
|
return errors.New("invalid configuration file: 'settings' or 'server' fields are missing. Please ensure you are importing a file generated by the 'config export' command")
|
||||||
|
}
|
||||||
|
|
||||||
file.Settings.Key = key
|
file.Settings.Key = key
|
||||||
err = d.store.Settings.Save(file.Settings)
|
err = st.Settings.Save(file.Settings)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
err = d.store.Settings.SaveServer(file.Server)
|
|
||||||
checkErr(err)
|
|
||||||
|
|
||||||
var rawAuther interface{}
|
|
||||||
if filepath.Ext(args[0]) != ".json" { //nolint:goconst
|
|
||||||
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
|
|
||||||
} else {
|
|
||||||
rawAuther = file.Auther
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = st.Settings.SaveServer(file.Server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawAuther = file.Auther
|
||||||
|
|
||||||
var auther auth.Auther
|
var auther auth.Auther
|
||||||
|
var autherErr error
|
||||||
switch file.Settings.AuthMethod {
|
switch file.Settings.AuthMethod {
|
||||||
case auth.MethodJSONAuth:
|
case auth.MethodJSONAuth:
|
||||||
auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(auth.JSONAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.JSONAuth)
|
||||||
case auth.MethodNoAuth:
|
case auth.MethodNoAuth:
|
||||||
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(auth.NoAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.NoAuth)
|
||||||
case auth.MethodProxyAuth:
|
case auth.MethodProxyAuth:
|
||||||
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(auth.ProxyAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.ProxyAuth)
|
||||||
|
case auth.MethodHookAuth:
|
||||||
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(&auth.HookAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.HookAuth)
|
||||||
default:
|
default:
|
||||||
checkErr(errors.New("invalid auth method"))
|
return errors.New("invalid auth method")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Auth.Save(auther)
|
if autherErr != nil {
|
||||||
checkErr(err)
|
return autherErr
|
||||||
|
}
|
||||||
|
|
||||||
printSettings(file.Server, file.Settings, auther)
|
err = st.Auth.Save(auther)
|
||||||
}, pythonConfig{allowNoDB: true}),
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return printSettings(file.Server, file.Settings, auther)
|
||||||
|
}, storeOptions{allowsNoDatabase: true}),
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuther(sample auth.Auther, data interface{}) interface{} {
|
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
|
||||||
authType := reflect.TypeOf(sample)
|
authType := reflect.TypeOf(sample)
|
||||||
auther := reflect.New(authType).Interface()
|
auther := reflect.New(authType).Interface()
|
||||||
bytes, err := json.Marshal(data)
|
bytes, err := json.Marshal(data)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
err = json.Unmarshal(bytes, &auther)
|
err = json.Unmarshal(bytes, &auther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
return auther
|
return nil, err
|
||||||
|
}
|
||||||
|
return auther, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
|
@ -23,48 +22,40 @@ this options can be changed in the future with the command
|
||||||
to the defaults when creating new users and you don't
|
to the defaults when creating new users and you don't
|
||||||
override the options.`,
|
override the options.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
|
||||||
defaults := settings.UserDefaults{}
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
getUserDefaults(flags, &defaults, true)
|
|
||||||
authMethod, auther := getAuthentication(flags)
|
|
||||||
|
|
||||||
s := &settings.Settings{
|
// Initialize config
|
||||||
Key: generateKey(),
|
s := &settings.Settings{Key: generateKey()}
|
||||||
Signup: mustGetBool(flags, "signup"),
|
ser := &settings.Server{}
|
||||||
Shell: strings.Split(strings.TrimSpace(mustGetString(flags, "shell")), " "),
|
|
||||||
AuthMethod: authMethod,
|
// Fill config with options
|
||||||
Defaults: defaults,
|
auther, err := getSettings(flags, s, ser, nil, true)
|
||||||
Branding: settings.Branding{
|
if err != nil {
|
||||||
Name: mustGetString(flags, "branding.name"),
|
return err
|
||||||
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
|
|
||||||
Files: mustGetString(flags, "branding.files"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ser := &settings.Server{
|
// Save updated config
|
||||||
Address: mustGetString(flags, "address"),
|
err = st.Settings.Save(s)
|
||||||
Socket: mustGetString(flags, "socket"),
|
if err != nil {
|
||||||
Root: mustGetString(flags, "root"),
|
return err
|
||||||
BaseURL: mustGetString(flags, "baseurl"),
|
|
||||||
TLSKey: mustGetString(flags, "key"),
|
|
||||||
TLSCert: mustGetString(flags, "cert"),
|
|
||||||
Port: mustGetString(flags, "port"),
|
|
||||||
Log: mustGetString(flags, "log"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := d.store.Settings.Save(s)
|
err = st.Settings.SaveServer(ser)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
err = d.store.Settings.SaveServer(ser)
|
return err
|
||||||
checkErr(err)
|
}
|
||||||
err = d.store.Auth.Save(auther)
|
|
||||||
checkErr(err)
|
err = st.Auth.Save(auther)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf(`
|
fmt.Printf(`
|
||||||
Congratulations! You've set up your database to use with File Browser.
|
Congratulations! You've set up your database to use with File Browser.
|
||||||
Now add your first user via 'filebrowser users new' and then you just
|
Now add your first user via 'filebrowser users add' and then you just
|
||||||
need to call the main command to boot up the server.
|
need to call the main command to boot up the server.
|
||||||
`)
|
`)
|
||||||
printSettings(ser, s, auther)
|
return printSettings(ser, s, auther)
|
||||||
}, pythonConfig{noDB: true}),
|
}, storeOptions{expectsNoDatabase: true}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -18,63 +15,47 @@ var configSetCmd = &cobra.Command{
|
||||||
Long: `Updates the configuration. Set the flags for the options
|
Long: `Updates the configuration. Set the flags for the options
|
||||||
you want to change. Other options will remain unchanged.`,
|
you want to change. Other options will remain unchanged.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
set, err := d.store.Settings.Get()
|
|
||||||
checkErr(err)
|
|
||||||
|
|
||||||
ser, err := d.store.Settings.GetServer()
|
// Read existing config
|
||||||
checkErr(err)
|
set, err := st.Settings.Get()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
hasAuth := false
|
ser, err := st.Settings.GetServer()
|
||||||
flags.Visit(func(flag *pflag.Flag) {
|
if err != nil {
|
||||||
switch flag.Name {
|
return err
|
||||||
case "baseurl":
|
}
|
||||||
ser.BaseURL = mustGetString(flags, flag.Name)
|
|
||||||
case "root":
|
|
||||||
ser.Root = mustGetString(flags, flag.Name)
|
|
||||||
case "socket":
|
|
||||||
ser.Socket = mustGetString(flags, flag.Name)
|
|
||||||
case "cert":
|
|
||||||
ser.TLSCert = mustGetString(flags, flag.Name)
|
|
||||||
case "key":
|
|
||||||
ser.TLSKey = mustGetString(flags, flag.Name)
|
|
||||||
case "address":
|
|
||||||
ser.Address = mustGetString(flags, flag.Name)
|
|
||||||
case "port":
|
|
||||||
ser.Port = mustGetString(flags, flag.Name)
|
|
||||||
case "log":
|
|
||||||
ser.Log = mustGetString(flags, flag.Name)
|
|
||||||
case "signup":
|
|
||||||
set.Signup = mustGetBool(flags, flag.Name)
|
|
||||||
case "auth.method":
|
|
||||||
hasAuth = true
|
|
||||||
case "shell":
|
|
||||||
set.Shell = strings.Split(strings.TrimSpace(mustGetString(flags, flag.Name)), " ")
|
|
||||||
case "branding.name":
|
|
||||||
set.Branding.Name = mustGetString(flags, flag.Name)
|
|
||||||
case "branding.disableExternal":
|
|
||||||
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
|
|
||||||
case "branding.files":
|
|
||||||
set.Branding.Files = mustGetString(flags, flag.Name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
getUserDefaults(flags, &set.Defaults, false)
|
auther, err := st.Auth.Get(set.AuthMethod)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// read the defaults
|
// Get updated config
|
||||||
auther, err := d.store.Auth.Get(set.AuthMethod)
|
auther, err = getSettings(flags, set, ser, auther, false)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// check if there are new flags for existing auth method
|
// Save updated config
|
||||||
set.AuthMethod, auther = getAuthentication(flags, hasAuth, set, auther)
|
err = st.Auth.Save(auther)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = d.store.Auth.Save(auther)
|
err = st.Settings.Save(set)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
err = d.store.Settings.Save(set)
|
return err
|
||||||
checkErr(err)
|
}
|
||||||
err = d.store.Settings.SaveServer(ser)
|
|
||||||
checkErr(err)
|
err = st.Settings.SaveServer(ser)
|
||||||
printSettings(ser, set, auther)
|
if err != nil {
|
||||||
}, pythonConfig{}),
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return printSettings(ser, set, auther)
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
165
cmd/docs.go
165
cmd/docs.go
|
|
@ -3,137 +3,80 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
"sort"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/cobra/doc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(docsCmd)
|
rootCmd.AddCommand(docsCmd)
|
||||||
docsCmd.Flags().StringP("path", "p", "./docs", "path to save the docs")
|
docsCmd.Flags().String("out", "docs/cli", "directory to write the docs to")
|
||||||
}
|
|
||||||
|
|
||||||
func printToc(names []string) {
|
|
||||||
for i, name := range names {
|
|
||||||
name = strings.TrimSuffix(name, filepath.Ext(name))
|
|
||||||
name = strings.Replace(name, "-", " ", -1)
|
|
||||||
names[i] = name
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(names)
|
|
||||||
|
|
||||||
toc := ""
|
|
||||||
for _, name := range names {
|
|
||||||
toc += "* [" + name + "](cli/" + strings.Replace(name, " ", "-", -1) + ".md)\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(toc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var docsCmd = &cobra.Command{
|
var docsCmd = &cobra.Command{
|
||||||
Use: "docs",
|
Use: "docs",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
dir := mustGetString(cmd.Flags(), "path")
|
outputDir, err := cmd.Flags().GetString("out")
|
||||||
generateDocs(rootCmd, dir)
|
if err != nil {
|
||||||
names := []string{}
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
tempDir, err := os.MkdirTemp(os.TempDir(), "filebrowser-docs-")
|
||||||
if err != nil || info.IsDir() {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
rootCmd.Root().DisableAutoGenTag = true
|
||||||
|
|
||||||
|
err = doc.GenMarkdownTreeCustom(cmd.Root(), tempDir, func(_ string) string {
|
||||||
|
return ""
|
||||||
|
}, func(s string) string {
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(tempDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
headerRegex := regexp.MustCompile(`(?m)^(##)(.*)$`)
|
||||||
|
linkRegex := regexp.MustCompile(`\(filebrowser(.*)\.md\)`)
|
||||||
|
|
||||||
|
fmt.Println("Generated Documents:")
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
srcPath := path.Join(tempDir, entry.Name())
|
||||||
|
dstPath := path.Join(outputDir, strings.ReplaceAll(entry.Name(), "_", "-"))
|
||||||
|
|
||||||
|
data, err := os.ReadFile(srcPath)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(info.Name(), "filebrowser") {
|
data = headerRegex.ReplaceAll(data, []byte("#$2"))
|
||||||
return nil
|
data = linkRegex.ReplaceAllFunc(data, func(b []byte) []byte {
|
||||||
|
return bytes.ReplaceAll(b, []byte("_"), []byte("-"))
|
||||||
|
})
|
||||||
|
data = bytes.ReplaceAll(data, []byte("## SEE ALSO"), []byte("## See Also"))
|
||||||
|
|
||||||
|
err = os.WriteFile(dstPath, data, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
names = append(names, info.Name())
|
fmt.Println("- " + dstPath)
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
checkErr(err)
|
|
||||||
printToc(names)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateDocs(cmd *cobra.Command, dir string) {
|
|
||||||
for _, c := range cmd.Commands() {
|
|
||||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
generateDocs(c, dir)
|
return nil
|
||||||
}
|
},
|
||||||
|
|
||||||
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
|
|
||||||
filename := filepath.Join(dir, basename)
|
|
||||||
f, err := os.Create(filename)
|
|
||||||
checkErr(err)
|
|
||||||
defer f.Close()
|
|
||||||
generateMarkdown(cmd, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
|
||||||
cmd.InitDefaultHelpCmd()
|
|
||||||
cmd.InitDefaultHelpFlag()
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
name := cmd.CommandPath()
|
|
||||||
|
|
||||||
short := cmd.Short
|
|
||||||
long := cmd.Long
|
|
||||||
if long == "" {
|
|
||||||
long = short
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString("---\ndescription: " + short + "\n---\n\n")
|
|
||||||
buf.WriteString("# " + name + "\n\n")
|
|
||||||
buf.WriteString("## Synopsis\n\n")
|
|
||||||
buf.WriteString(long + "\n\n")
|
|
||||||
|
|
||||||
if cmd.Runnable() {
|
|
||||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cmd.Example) > 0 {
|
|
||||||
buf.WriteString("## Examples\n\n")
|
|
||||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
|
|
||||||
}
|
|
||||||
|
|
||||||
printOptions(buf, cmd)
|
|
||||||
_, err := buf.WriteTo(w)
|
|
||||||
checkErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
|
|
||||||
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
|
|
||||||
_, _ = buf.WriteString("|------|-----------|-------|\n")
|
|
||||||
|
|
||||||
fs.VisitAll(func(f *pflag.Flag) {
|
|
||||||
_, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
|
|
||||||
flags := cmd.NonInheritedFlags()
|
|
||||||
flags.SetOutput(buf)
|
|
||||||
if flags.HasAvailableFlags() {
|
|
||||||
buf.WriteString("## Options\n\n")
|
|
||||||
generateFlagsTable(flags, buf)
|
|
||||||
buf.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
parentFlags := cmd.InheritedFlags()
|
|
||||||
parentFlags.SetOutput(buf)
|
|
||||||
if parentFlags.HasAvailableFlags() {
|
|
||||||
buf.WriteString("### Inherited\n\n")
|
|
||||||
generateFlagsTable(parentFlags, buf)
|
|
||||||
buf.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,12 @@ var hashCmd = &cobra.Command{
|
||||||
Short: "Hashes a password",
|
Short: "Hashes a password",
|
||||||
Long: `Hashes a password using bcrypt algorithm.`,
|
Long: `Hashes a password using bcrypt algorithm.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
pwd, err := users.HashPwd(args[0])
|
pwd, err := users.HashPwd(args[0])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Println(pwd)
|
fmt.Println(pwd)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
511
cmd/root.go
511
cmd/root.go
|
|
@ -1,9 +1,12 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -12,41 +15,88 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
v "github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/diskcache"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/frontend"
|
||||||
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/img"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfgFile string
|
flagNamesMigrations = map[string]string{
|
||||||
|
"file-mode": "fileMode",
|
||||||
|
"dir-mode": "dirMode",
|
||||||
|
"hide-login-button": "hideLoginButton",
|
||||||
|
"create-user-dir": "createUserDir",
|
||||||
|
"minimum-password-length": "minimumPasswordLength",
|
||||||
|
"socket-perm": "socketPerm",
|
||||||
|
"disable-thumbnails": "disableThumbnails",
|
||||||
|
"disable-preview-resize": "disablePreviewResize",
|
||||||
|
"disable-exec": "disableExec",
|
||||||
|
"disable-type-detection-by-header": "disableTypeDetectionByHeader",
|
||||||
|
"img-processors": "imageProcessors",
|
||||||
|
"cache-dir": "cacheDir",
|
||||||
|
"redis-cache-url": "redisCacheUrl",
|
||||||
|
"token-expiration-time": "tokenExpirationTime",
|
||||||
|
"baseurl": "baseURL",
|
||||||
|
}
|
||||||
|
|
||||||
|
warnedFlags = map[string]bool{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO(remove): remove after July 2026.
|
||||||
|
func migrateFlagNames(_ *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||||
|
if newName, ok := flagNamesMigrations[name]; ok {
|
||||||
|
|
||||||
|
if !warnedFlags[name] {
|
||||||
|
warnedFlags[name] = true
|
||||||
|
log.Printf("DEPRECATION NOTICE: Flag --%s has been deprecated, use --%s instead\n", name, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
name = newName
|
||||||
|
}
|
||||||
|
|
||||||
|
return pflag.NormalizedName(name)
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
rootCmd.SilenceUsage = true
|
||||||
|
rootCmd.SetGlobalNormalizationFunc(migrateFlagNames)
|
||||||
|
|
||||||
|
cobra.MousetrapHelpText = ""
|
||||||
|
|
||||||
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
||||||
|
|
||||||
flags := rootCmd.Flags()
|
// Flags available across the whole program
|
||||||
persistent := rootCmd.PersistentFlags()
|
persistent := rootCmd.PersistentFlags()
|
||||||
|
persistent.StringP("config", "c", "", "config file path")
|
||||||
persistent.StringVarP(&cfgFile, "config", "c", "", "config file path")
|
|
||||||
persistent.StringP("database", "d", "./filebrowser.db", "database path")
|
persistent.StringP("database", "d", "./filebrowser.db", "database path")
|
||||||
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
|
|
||||||
flags.String("username", "admin", "username for the first user when using quick config")
|
|
||||||
flags.String("password", "", "hashed password for the first user when using quick config (default \"admin\")")
|
|
||||||
|
|
||||||
|
// Runtime flags for the root command
|
||||||
|
flags := rootCmd.Flags()
|
||||||
|
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
|
||||||
|
flags.String("username", "admin", "username for the first user when using quick setup")
|
||||||
|
flags.String("password", "", "hashed password for the first user when using quick setup")
|
||||||
|
flags.Uint32("socketPerm", 0666, "unix socket file permissions")
|
||||||
|
flags.String("cacheDir", "", "file cache directory (disabled if empty)")
|
||||||
|
flags.String("redisCacheUrl", "", "redis cache URL (for multi-instance deployments), e.g. redis://user:pass@host:port")
|
||||||
|
flags.Int("imageProcessors", 4, "image processors count")
|
||||||
addServerFlags(flags)
|
addServerFlags(flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addServerFlags adds server related flags to the given FlagSet. These flags are available
|
||||||
|
// in both the root command, config set and config init commands.
|
||||||
func addServerFlags(flags *pflag.FlagSet) {
|
func addServerFlags(flags *pflag.FlagSet) {
|
||||||
flags.StringP("address", "a", "127.0.0.1", "address to listen on")
|
flags.StringP("address", "a", "127.0.0.1", "address to listen on")
|
||||||
flags.StringP("log", "l", "stdout", "log output")
|
flags.StringP("log", "l", "stdout", "log output")
|
||||||
|
|
@ -55,14 +105,21 @@ func addServerFlags(flags *pflag.FlagSet) {
|
||||||
flags.StringP("key", "k", "", "tls key")
|
flags.StringP("key", "k", "", "tls key")
|
||||||
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
||||||
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
||||||
flags.StringP("baseurl", "b", "", "base url")
|
flags.StringP("baseURL", "b", "", "base url")
|
||||||
|
flags.String("tokenExpirationTime", "2h", "user session timeout")
|
||||||
|
flags.Bool("disableThumbnails", false, "disable image thumbnails")
|
||||||
|
flags.Bool("disablePreviewResize", false, "disable resize of image previews")
|
||||||
|
flags.Bool("disableExec", true, "disables Command Runner feature")
|
||||||
|
flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers")
|
||||||
|
flags.Bool("disableImageResolutionCalc", false, "disables image resolution calculation by reading image files")
|
||||||
|
flags.Bool("followExternalSymlinks", false, "follow symlinks whose target is outside the user scope (unsafe)")
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "filebrowser",
|
Use: "filebrowser",
|
||||||
Short: "A stylish web-based file browser",
|
Short: "A stylish web-based file browser",
|
||||||
Long: `File Browser CLI lets you create the database to use with File Browser,
|
Long: `File Browser CLI lets you create the database to use with File Browser,
|
||||||
manage your users and all the configurations without acessing the
|
manage your users and all the configurations without accessing the
|
||||||
web interface.
|
web interface.
|
||||||
|
|
||||||
If you've never run File Browser, you'll need to have a database for
|
If you've never run File Browser, you'll need to have a database for
|
||||||
|
|
@ -70,44 +127,80 @@ it. Don't worry: you don't need to setup a separate database server.
|
||||||
We're using Bolt DB which is a single file database and all managed
|
We're using Bolt DB which is a single file database and all managed
|
||||||
by ourselves.
|
by ourselves.
|
||||||
|
|
||||||
For this specific command, all the flags you have available (except
|
For this command, all flags are available as environmental variables,
|
||||||
"config" for the configuration file), can be given either through
|
except for "--config", which specifies the configuration file to use.
|
||||||
environment variables or configuration files.
|
The environment variables are prefixed by "FB_" followed by the flag name in
|
||||||
|
UPPER_SNAKE_CASE. For example, the flag "--disablePreviewResize" is available
|
||||||
|
as FB_DISABLE_PREVIEW_RESIZE.
|
||||||
|
|
||||||
If you don't set "config", it will look for a configuration file called
|
If "--config" is not specified, File Browser will look for a configuration
|
||||||
.filebrowser.{json, toml, yaml, yml} in the following directories:
|
file named .filebrowser.{json, toml, yaml, yml} in the following directories:
|
||||||
|
|
||||||
- ./
|
- ./
|
||||||
- $HOME/
|
- $HOME/
|
||||||
- /etc/filebrowser/
|
- /etc/filebrowser/
|
||||||
|
|
||||||
|
**Note:** Only the options listed below can be set via the config file or
|
||||||
|
environment variables. Other configuration options live exclusively in the
|
||||||
|
database and so they must be set by the "config set" or "config
|
||||||
|
import" commands.
|
||||||
|
|
||||||
The precedence of the configuration values are as follows:
|
The precedence of the configuration values are as follows:
|
||||||
|
|
||||||
- flags
|
- Flags
|
||||||
- environment variables
|
- Environment variables
|
||||||
- configuration file
|
- Configuration file
|
||||||
- database values
|
- Database values
|
||||||
- defaults
|
- Defaults
|
||||||
|
|
||||||
The environment variables are prefixed by "FB_" followed by the option
|
|
||||||
name in caps. So to set "database" via an env variable, you should
|
|
||||||
set FB_DATABASE.
|
|
||||||
|
|
||||||
Also, if the database path doesn't exist, File Browser will enter into
|
Also, if the database path doesn't exist, File Browser will enter into
|
||||||
the quick setup mode and a new database will be bootstraped and a new
|
the quick setup mode and a new database will be bootstrapped and a new
|
||||||
user created with the credentials from options "username" and "password".`,
|
user created with the credentials from options "username" and "password".`,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withViperAndStore(func(_ *cobra.Command, _ []string, v *viper.Viper, st *store) error {
|
||||||
log.Println(cfgFile)
|
if !st.databaseExisted {
|
||||||
|
err := quickSetup(v, st.Storage)
|
||||||
if !d.hadDB {
|
if err != nil {
|
||||||
quickSetup(cmd.Flags(), d)
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server := getRunParams(cmd.Flags(), d.store)
|
// build img service
|
||||||
|
imgWorkersCount := v.GetInt("imageProcessors")
|
||||||
|
if imgWorkersCount < 1 {
|
||||||
|
return errors.New("image resize workers count could not be < 1")
|
||||||
|
}
|
||||||
|
imageService := img.New(imgWorkersCount)
|
||||||
|
|
||||||
|
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
||||||
|
cacheDir := v.GetString("cacheDir")
|
||||||
|
if cacheDir != "" {
|
||||||
|
if err := os.MkdirAll(cacheDir, 0700); err != nil {
|
||||||
|
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
|
||||||
|
}
|
||||||
|
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
redisCacheURL := v.GetString("redisCacheUrl")
|
||||||
|
uploadCache, err := fbhttp.NewUploadCache(redisCacheURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize upload cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := getServerSettings(v, st.Storage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
setupLog(server.Log)
|
setupLog(server.Log)
|
||||||
|
|
||||||
|
log.Println("NOTICE: File Browser is being wound down.")
|
||||||
|
log.Println("NOTICE: The project is archived on 2026-09-01, after which there will be no")
|
||||||
|
log.Println("NOTICE: further releases and no security fixes. Known unfixed issues are at")
|
||||||
|
log.Println("NOTICE: https://github.com/filebrowser/filebrowser/security/advisories")
|
||||||
|
|
||||||
root, err := filepath.Abs(server.Root)
|
root, err := filepath.Abs(server.Root)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
server.Root = root
|
server.Root = root
|
||||||
|
|
||||||
adr := server.Address + ":" + server.Port
|
adr := server.Address + ":" + server.Port
|
||||||
|
|
@ -117,87 +210,162 @@ user created with the credentials from options "username" and "password".`,
|
||||||
switch {
|
switch {
|
||||||
case server.Socket != "":
|
case server.Socket != "":
|
||||||
listener, err = net.Listen("unix", server.Socket)
|
listener, err = net.Listen("unix", server.Socket)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
socketPerm := v.GetUint32("socketPerm")
|
||||||
|
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case server.TLSKey != "" && server.TLSCert != "":
|
case server.TLSKey != "" && server.TLSCert != "":
|
||||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
|
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) //nolint:shadow
|
return err
|
||||||
checkErr(err)
|
}
|
||||||
|
listener, err = tls.Listen("tcp", adr, &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
Certificates: []tls.Certificate{cer}},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
listener, err = net.Listen("tcp", adr) //nolint:shadow
|
listener, err = net.Listen("tcp", adr)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sigc := make(chan os.Signal, 1)
|
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
|
||||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
if err != nil {
|
||||||
go cleanupHandler(listener, sigc)
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
handler, err := fbhttp.NewHandler(d.store, server)
|
handler, err := fbhttp.NewHandler(imageService, fileCache, uploadCache, st.Storage, server, assetsFs)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
log.Println("Listening on", listener.Addr().String())
|
log.Println("Listening on", listener.Addr().String())
|
||||||
if err := http.Serve(listener, handler); err != nil {
|
srv := &http.Server{
|
||||||
log.Fatal(err)
|
Handler: handler,
|
||||||
|
ReadHeaderTimeout: 60 * time.Second,
|
||||||
}
|
}
|
||||||
}, pythonConfig{allowNoDB: true}),
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Fatalf("HTTP server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Stopped serving new connections.")
|
||||||
|
}()
|
||||||
|
|
||||||
|
sigc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigc,
|
||||||
|
os.Interrupt,
|
||||||
|
syscall.SIGHUP,
|
||||||
|
syscall.SIGINT,
|
||||||
|
syscall.SIGTERM,
|
||||||
|
syscall.SIGQUIT,
|
||||||
|
)
|
||||||
|
sig := <-sigc
|
||||||
|
log.Println("Got signal:", sig)
|
||||||
|
|
||||||
|
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer shutdownRelease()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Fatalf("HTTP shutdown error: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("Graceful shutdown complete.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, storeOptions{allowsNoDatabase: true}),
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfacer
|
func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, error) {
|
||||||
sig := <-c
|
|
||||||
log.Printf("Caught signal %s: shutting down.", sig)
|
|
||||||
listener.Close()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:gocyclo
|
|
||||||
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|
||||||
server, err := st.Settings.GetServer()
|
server, err := st.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
if val, set := getParamB(flags, "root"); set {
|
|
||||||
server.Root = val
|
|
||||||
}
|
|
||||||
|
|
||||||
if val, set := getParamB(flags, "baseurl"); set {
|
|
||||||
server.BaseURL = val
|
|
||||||
}
|
|
||||||
|
|
||||||
if val, set := getParamB(flags, "log"); set {
|
|
||||||
server.Log = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isSocketSet := false
|
isSocketSet := false
|
||||||
isAddrSet := false
|
isAddrSet := false
|
||||||
|
|
||||||
if val, set := getParamB(flags, "address"); set {
|
if v.IsSet("address") {
|
||||||
server.Address = val
|
server.Address = v.GetString("address")
|
||||||
isAddrSet = isAddrSet || set
|
isAddrSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if val, set := getParamB(flags, "port"); set {
|
if v.IsSet("log") {
|
||||||
server.Port = val
|
server.Log = v.GetString("log")
|
||||||
isAddrSet = isAddrSet || set
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if val, set := getParamB(flags, "key"); set {
|
if v.IsSet("port") {
|
||||||
server.TLSKey = val
|
server.Port = v.GetString("port")
|
||||||
isAddrSet = isAddrSet || set
|
isAddrSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if val, set := getParamB(flags, "cert"); set {
|
if v.IsSet("cert") {
|
||||||
server.TLSCert = val
|
server.TLSCert = v.GetString("cert")
|
||||||
isAddrSet = isAddrSet || set
|
isAddrSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if val, set := getParamB(flags, "socket"); set {
|
if v.IsSet("key") {
|
||||||
server.Socket = val
|
server.TLSKey = v.GetString("key")
|
||||||
isSocketSet = isSocketSet || set
|
isAddrSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("root") {
|
||||||
|
server.Root = v.GetString("root")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("socket") {
|
||||||
|
server.Socket = v.GetString("socket")
|
||||||
|
isSocketSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("baseURL") {
|
||||||
|
server.BaseURL = v.GetString("baseURL")
|
||||||
|
// TODO(remove): remove after July 2026.
|
||||||
|
} else if v := os.Getenv("FB_BASEURL"); v != "" {
|
||||||
|
log.Println("DEPRECATION NOTICE: Environment variable FB_BASEURL has been deprecated, use FB_BASE_URL instead")
|
||||||
|
server.BaseURL = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("tokenExpirationTime") {
|
||||||
|
server.TokenExpirationTime = v.GetString("tokenExpirationTime")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("disableThumbnails") {
|
||||||
|
server.EnableThumbnails = !v.GetBool("disableThumbnails")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("disablePreviewResize") {
|
||||||
|
server.ResizePreview = !v.GetBool("disablePreviewResize")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("disableTypeDetectionByHeader") {
|
||||||
|
server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("disableImageResolutionCalc") {
|
||||||
|
server.ImageResolutionCal = !v.GetBool("disableImageResolutionCalc")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("disableExec") {
|
||||||
|
server.EnableExec = !v.GetBool("disableExec")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.IsSet("followExternalSymlinks") {
|
||||||
|
server.FollowExternalSymlinks = v.GetBool("followExternalSymlinks")
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAddrSet && isSocketSet {
|
if isAddrSet && isSocketSet {
|
||||||
checkErr(errors.New("--socket flag cannot be used with --address, --port, --key nor --cert"))
|
return nil, errors.New("--socket flag cannot be used with --address, --port, --key nor --cert")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not use saved Socket if address was manually set.
|
// Do not use saved Socket if address was manually set.
|
||||||
|
|
@ -205,36 +373,33 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
||||||
server.Socket = ""
|
server.Socket = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return server
|
if server.EnableExec {
|
||||||
}
|
log.Println("WARNING: Command Runner feature enabled!")
|
||||||
|
log.Println("WARNING: This feature has known security vulnerabilities and should not")
|
||||||
// getParamB returns a parameter as a string and a boolean to tell if it is different from the default
|
log.Println("WARNING: you fully understand the risks involved. For more information")
|
||||||
//
|
log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199")
|
||||||
// NOTE: we could simply bind the flags to viper and use IsSet.
|
|
||||||
// Although there is a bug on Viper that always returns true on IsSet
|
|
||||||
// if a flag is binded. Our alternative way is to manually check
|
|
||||||
// the flag and then the value from env/config/gotten by viper.
|
|
||||||
// https://github.com/spf13/viper/pull/331
|
|
||||||
func getParamB(flags *pflag.FlagSet, key string) (string, bool) {
|
|
||||||
value, _ := flags.GetString(key)
|
|
||||||
|
|
||||||
// If set on Flags, use it.
|
|
||||||
if flags.Changed(key) {
|
|
||||||
return value, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If set through viper (env, config), return it.
|
if server.FollowExternalSymlinks {
|
||||||
if v.IsSet(key) {
|
log.Println("WARNING: Following external symlinks enabled!")
|
||||||
return v.GetString(key), true
|
log.Println("WARNING: Symlinks pointing outside a user's scope will be followed,")
|
||||||
|
log.Println("WARNING: which can expose files outside that scope. Only enable this if")
|
||||||
|
log.Println("WARNING: you fully understand and trust the contents of every user scope.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise use default value on flags.
|
if set, err := st.Settings.Get(); err == nil && set.Signup {
|
||||||
return value, false
|
scope := strings.TrimSpace(set.Defaults.Scope)
|
||||||
}
|
scopeIsRoot := scope == "" || scope == "." || scope == "/"
|
||||||
|
|
||||||
func getParam(flags *pflag.FlagSet, key string) string {
|
if !set.CreateUserDir && scopeIsRoot {
|
||||||
val, _ := getParamB(flags, key)
|
log.Println("WARNING: Signup is enabled without createUserDir and the default scope is")
|
||||||
return val
|
log.Println("WARNING: the server root, so every self-registered user can read, modify and")
|
||||||
|
log.Println("WARNING: delete all files File Browser serves, including other users' files.")
|
||||||
|
log.Println("WARNING: Enable createUserDir, or set a default scope other than the root.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupLog(logMethod string) {
|
func setupLog(logMethod string) {
|
||||||
|
|
@ -244,7 +409,7 @@ func setupLog(logMethod string) {
|
||||||
case "stderr":
|
case "stderr":
|
||||||
log.SetOutput(os.Stderr)
|
log.SetOutput(os.Stderr)
|
||||||
case "":
|
case "":
|
||||||
log.SetOutput(ioutil.Discard)
|
log.SetOutput(io.Discard)
|
||||||
default:
|
default:
|
||||||
log.SetOutput(&lumberjack.Logger{
|
log.SetOutput(&lumberjack.Logger{
|
||||||
Filename: logMethod,
|
Filename: logMethod,
|
||||||
|
|
@ -255,14 +420,22 @@ func setupLog(logMethod string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
func quickSetup(v *viper.Viper, s *storage.Storage) error {
|
||||||
|
log.Println("Performing quick setup")
|
||||||
|
|
||||||
set := &settings.Settings{
|
set := &settings.Settings{
|
||||||
Key: generateKey(),
|
Key: generateKey(),
|
||||||
Signup: false,
|
Signup: false,
|
||||||
CreateUserDir: false,
|
HideLoginButton: true,
|
||||||
|
CreateUserDir: false,
|
||||||
|
MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
|
||||||
|
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
|
||||||
Defaults: settings.UserDefaults{
|
Defaults: settings.UserDefaults{
|
||||||
Scope: ".",
|
Scope: ".",
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
|
SingleClick: false,
|
||||||
|
RedirectAfterCopyMove: true,
|
||||||
|
AceEditorTheme: v.GetString("defaults.aceEditorTheme"),
|
||||||
Perm: users.Permissions{
|
Perm: users.Permissions{
|
||||||
Admin: false,
|
Admin: false,
|
||||||
Execute: true,
|
Execute: true,
|
||||||
|
|
@ -274,40 +447,73 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
||||||
Download: true,
|
Download: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
AuthMethod: "",
|
||||||
|
Branding: settings.Branding{},
|
||||||
|
Tus: settings.Tus{
|
||||||
|
ChunkSize: settings.DefaultTusChunkSize,
|
||||||
|
RetryCount: settings.DefaultTusRetryCount,
|
||||||
|
},
|
||||||
|
Commands: nil,
|
||||||
|
Shell: nil,
|
||||||
|
Rules: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if _, noauth := getParamB(flags, "noauth"); noauth {
|
if v.GetBool("noauth") {
|
||||||
set.AuthMethod = auth.MethodNoAuth
|
set.AuthMethod = auth.MethodNoAuth
|
||||||
err = d.store.Auth.Save(&auth.NoAuth{})
|
err = s.Auth.Save(&auth.NoAuth{})
|
||||||
} else {
|
} else {
|
||||||
set.AuthMethod = auth.MethodJSONAuth
|
set.AuthMethod = auth.MethodJSONAuth
|
||||||
err = d.store.Auth.Save(&auth.JSONAuth{})
|
err = s.Auth.Save(&auth.JSONAuth{})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
err = s.Settings.Save(set)
|
||||||
err = d.store.Settings.Save(set)
|
if err != nil {
|
||||||
checkErr(err)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ser := &settings.Server{
|
ser := &settings.Server{
|
||||||
BaseURL: getParam(flags, "baseurl"),
|
BaseURL: v.GetString("baseURL"),
|
||||||
Port: getParam(flags, "port"),
|
Port: v.GetString("port"),
|
||||||
Log: getParam(flags, "log"),
|
Log: v.GetString("log"),
|
||||||
TLSKey: getParam(flags, "key"),
|
TLSKey: v.GetString("key"),
|
||||||
TLSCert: getParam(flags, "cert"),
|
TLSCert: v.GetString("cert"),
|
||||||
Address: getParam(flags, "address"),
|
Address: v.GetString("address"),
|
||||||
Root: getParam(flags, "root"),
|
Root: v.GetString("root"),
|
||||||
|
TokenExpirationTime: v.GetString("tokenExpirationTime"),
|
||||||
|
EnableThumbnails: !v.GetBool("disableThumbnails"),
|
||||||
|
ResizePreview: !v.GetBool("disablePreviewResize"),
|
||||||
|
EnableExec: !v.GetBool("disableExec"),
|
||||||
|
TypeDetectionByHeader: !v.GetBool("disableTypeDetectionByHeader"),
|
||||||
|
ImageResolutionCal: !v.GetBool("disableImageResolutionCalc"),
|
||||||
|
FollowExternalSymlinks: v.GetBool("followExternalSymlinks"),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Settings.SaveServer(ser)
|
err = s.Settings.SaveServer(ser)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
username := getParam(flags, "username")
|
username := v.GetString("username")
|
||||||
password := getParam(flags, "password")
|
password := v.GetString("password")
|
||||||
|
|
||||||
if password == "" {
|
if password == "" {
|
||||||
password, err = users.HashPwd("admin")
|
var pwd string
|
||||||
checkErr(err)
|
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("User '%s' initialized with randomly generated password: %s\n", username, pwd)
|
||||||
|
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("User '%s' initialize wth user-provided password\n", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
if username == "" || password == "" {
|
if username == "" || password == "" {
|
||||||
|
|
@ -323,32 +529,5 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
||||||
set.Defaults.Apply(user)
|
set.Defaults.Apply(user)
|
||||||
user.Perm.Admin = true
|
user.Perm.Admin = true
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
return s.Users.Save(user)
|
||||||
checkErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initConfig() {
|
|
||||||
if cfgFile == "" {
|
|
||||||
home, err := homedir.Dir()
|
|
||||||
checkErr(err)
|
|
||||||
v.AddConfigPath(".")
|
|
||||||
v.AddConfigPath(home)
|
|
||||||
v.AddConfigPath("/etc/filebrowser/")
|
|
||||||
v.SetConfigName(".filebrowser")
|
|
||||||
} else {
|
|
||||||
v.SetConfigFile(cfgFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.SetEnvPrefix("FB")
|
|
||||||
v.AutomaticEnv()
|
|
||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
||||||
|
|
||||||
if err := v.ReadInConfig(); err != nil {
|
|
||||||
if _, ok := err.(v.ConfigParseError); ok {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
cfgFile = "No config file used"
|
|
||||||
} else {
|
|
||||||
cfgFile = "Using config file: " + v.ConfigFileUsed()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,27 +40,29 @@ including 'index_end'.`,
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||||
i, err := strconv.Atoi(args[0])
|
i, err := strconv.Atoi(args[0])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 2 { //nolint:mnd
|
if len(args) == 2 {
|
||||||
f, err = strconv.Atoi(args[1])
|
f, err = strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user := func(u *users.User) {
|
user := func(u *users.User) error {
|
||||||
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
|
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
|
||||||
err := d.store.Users.Save(u)
|
return st.Users.Save(u)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global := func(s *settings.Settings) {
|
global := func(s *settings.Settings) error {
|
||||||
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
|
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
|
||||||
err := d.store.Settings.Save(s)
|
return st.Settings.Save(s)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runRules(d.store, cmd, user, global)
|
return runRules(st.Storage, cmd, user, global)
|
||||||
}, pythonConfig{}),
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
56
cmd/rules.go
56
cmd/rules.go
|
|
@ -29,41 +29,63 @@ rules.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
|
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) error, globalFn func(*settings.Settings) error) error {
|
||||||
id := getUserIdentifier(cmd.Flags())
|
id, err := getUserIdentifier(cmd.Flags())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if id != nil {
|
if id != nil {
|
||||||
user, err := st.Users.Get("", id)
|
var user *users.User
|
||||||
checkErr(err)
|
user, err = st.Users.Get("", false, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if usersFn != nil {
|
if usersFn != nil {
|
||||||
usersFn(user)
|
err = usersFn(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printRules(user.Rules, id)
|
printRules(user.Rules, id)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := st.Settings.Get()
|
s, err := st.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if globalFn != nil {
|
if globalFn != nil {
|
||||||
globalFn(s)
|
err = globalFn(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printRules(s.Rules, id)
|
printRules(s.Rules, id)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
|
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
|
||||||
id := mustGetUint(flags, "id")
|
id, err := flags.GetUint("id")
|
||||||
username := mustGetString(flags, "username")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
if id != 0 {
|
|
||||||
return id
|
|
||||||
} else if username != "" {
|
|
||||||
return username
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
username, err := flags.GetString("username")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != 0 {
|
||||||
|
return id, nil
|
||||||
|
} else if username != "" {
|
||||||
|
return username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printRules(rulez []rules.Rule, id interface{}) {
|
func printRules(rulez []rules.Rule, id interface{}) {
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,19 @@ var rulesAddCmd = &cobra.Command{
|
||||||
Short: "Add a global rule or user rule",
|
Short: "Add a global rule or user rule",
|
||||||
Long: `Add a global rule or user rule.`,
|
Long: `Add a global rule or user rule.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||||
allow := mustGetBool(cmd.Flags(), "allow")
|
flags := cmd.Flags()
|
||||||
regex := mustGetBool(cmd.Flags(), "regex")
|
|
||||||
|
allow, err := flags.GetBool("allow")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
regex, err := flags.GetBool("regex")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
exp := args[0]
|
exp := args[0]
|
||||||
|
|
||||||
if regex {
|
if regex {
|
||||||
|
|
@ -41,18 +51,16 @@ var rulesAddCmd = &cobra.Command{
|
||||||
rule.Path = exp
|
rule.Path = exp
|
||||||
}
|
}
|
||||||
|
|
||||||
user := func(u *users.User) {
|
user := func(u *users.User) error {
|
||||||
u.Rules = append(u.Rules, rule)
|
u.Rules = append(u.Rules, rule)
|
||||||
err := d.store.Users.Save(u)
|
return st.Users.Save(u)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global := func(s *settings.Settings) {
|
global := func(s *settings.Settings) error {
|
||||||
s.Rules = append(s.Rules, rule)
|
s.Rules = append(s.Rules, rule)
|
||||||
err := d.store.Settings.Save(s)
|
return st.Settings.Save(s)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runRules(d.store, cmd, user, global)
|
return runRules(st.Storage, cmd, user, global)
|
||||||
}, pythonConfig{}),
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
|
||||||
Short: "List global rules or user specific rules",
|
Short: "List global rules or user specific rules",
|
||||||
Long: `List global rules or user specific rules.`,
|
Long: `List global rules or user specific rules.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
|
||||||
runRules(d.store, cmd, nil, nil)
|
return runRules(st.Storage, cmd, nil, nil)
|
||||||
}, pythonConfig{}),
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(upgradeCmd)
|
|
||||||
|
|
||||||
upgradeCmd.Flags().String("old.database", "", "")
|
|
||||||
upgradeCmd.Flags().String("old.config", "", "")
|
|
||||||
_ = upgradeCmd.MarkFlagRequired("old.database")
|
|
||||||
}
|
|
||||||
|
|
||||||
var upgradeCmd = &cobra.Command{
|
|
||||||
Use: "upgrade",
|
|
||||||
Short: "Upgrades an old configuration",
|
|
||||||
Long: `Upgrades an old configuration. This command DOES NOT
|
|
||||||
import share links because they are incompatible with
|
|
||||||
this version.`,
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
flags := cmd.Flags()
|
|
||||||
oldDB := mustGetString(flags, "old.database")
|
|
||||||
oldConf := mustGetString(flags, "old.config")
|
|
||||||
err := importer.Import(oldDB, oldConf, getParam(flags, "database"))
|
|
||||||
checkErr(err)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
81
cmd/users.go
81
cmd/users.go
|
|
@ -27,15 +27,17 @@ var usersCmd = &cobra.Command{
|
||||||
|
|
||||||
func printUsers(usrs []*users.User) {
|
func printUsers(usrs []*users.User) {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tRed. After C/M\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
||||||
|
|
||||||
for _, u := range usrs {
|
for _, u := range usrs {
|
||||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
||||||
u.ID,
|
u.ID,
|
||||||
u.Username,
|
u.Username,
|
||||||
u.Scope,
|
u.Scope,
|
||||||
u.Locale,
|
u.Locale,
|
||||||
u.ViewMode,
|
u.ViewMode,
|
||||||
|
u.SingleClick,
|
||||||
|
u.RedirectAfterCopyMove,
|
||||||
u.Perm.Admin,
|
u.Perm.Admin,
|
||||||
u.Perm.Execute,
|
u.Perm.Execute,
|
||||||
u.Perm.Create,
|
u.Perm.Create,
|
||||||
|
|
@ -52,7 +54,7 @@ func printUsers(usrs []*users.User) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUsernameOrID(arg string) (username string, id uint) {
|
func parseUsernameOrID(arg string) (username string, id uint) {
|
||||||
id64, err := strconv.ParseUint(arg, 10, 0)
|
id64, err := strconv.ParseUint(arg, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return arg, 0
|
return arg, 0
|
||||||
}
|
}
|
||||||
|
|
@ -75,50 +77,75 @@ func addUserFlags(flags *pflag.FlagSet) {
|
||||||
flags.String("scope", ".", "scope for users")
|
flags.String("scope", ".", "scope for users")
|
||||||
flags.String("locale", "en", "locale for users")
|
flags.String("locale", "en", "locale for users")
|
||||||
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
||||||
|
flags.Bool("singleClick", false, "use single clicks only")
|
||||||
|
flags.Bool("redirectAfterCopyMove", false, "redirect to destination after copy/move")
|
||||||
|
flags.Bool("dateFormat", false, "use date format (true for absolute time, false for relative)")
|
||||||
|
flags.Bool("hideDotfiles", false, "hide dotfiles in file listings")
|
||||||
|
flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
|
func getAndParseViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
|
||||||
viewMode := users.ViewMode(mustGetString(flags, "viewMode"))
|
viewModeStr, err := flags.GetString("viewMode")
|
||||||
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
|
if err != nil {
|
||||||
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\""))
|
return "", err
|
||||||
}
|
}
|
||||||
return viewMode
|
|
||||||
|
viewMode := users.ViewMode(viewModeStr)
|
||||||
|
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
|
||||||
|
return "", errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewMode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
|
||||||
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
|
errs := []error{}
|
||||||
|
|
||||||
visit := func(flag *pflag.Flag) {
|
visit := func(flag *pflag.Flag) {
|
||||||
|
var err error
|
||||||
switch flag.Name {
|
switch flag.Name {
|
||||||
case "scope":
|
case "scope":
|
||||||
defaults.Scope = mustGetString(flags, flag.Name)
|
defaults.Scope, err = flags.GetString(flag.Name)
|
||||||
case "locale":
|
case "locale":
|
||||||
defaults.Locale = mustGetString(flags, flag.Name)
|
defaults.Locale, err = flags.GetString(flag.Name)
|
||||||
case "viewMode":
|
case "viewMode":
|
||||||
defaults.ViewMode = getViewMode(flags)
|
defaults.ViewMode, err = getAndParseViewMode(flags)
|
||||||
|
case "singleClick":
|
||||||
|
defaults.SingleClick, err = flags.GetBool(flag.Name)
|
||||||
|
case "redirectAfterCopyMove":
|
||||||
|
defaults.RedirectAfterCopyMove, err = flags.GetBool(flag.Name)
|
||||||
|
case "aceEditorTheme":
|
||||||
|
defaults.AceEditorTheme, err = flags.GetString(flag.Name)
|
||||||
case "perm.admin":
|
case "perm.admin":
|
||||||
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
|
defaults.Perm.Admin, err = flags.GetBool(flag.Name)
|
||||||
case "perm.execute":
|
case "perm.execute":
|
||||||
defaults.Perm.Execute = mustGetBool(flags, flag.Name)
|
defaults.Perm.Execute, err = flags.GetBool(flag.Name)
|
||||||
case "perm.create":
|
case "perm.create":
|
||||||
defaults.Perm.Create = mustGetBool(flags, flag.Name)
|
defaults.Perm.Create, err = flags.GetBool(flag.Name)
|
||||||
case "perm.rename":
|
case "perm.rename":
|
||||||
defaults.Perm.Rename = mustGetBool(flags, flag.Name)
|
defaults.Perm.Rename, err = flags.GetBool(flag.Name)
|
||||||
case "perm.modify":
|
case "perm.modify":
|
||||||
defaults.Perm.Modify = mustGetBool(flags, flag.Name)
|
defaults.Perm.Modify, err = flags.GetBool(flag.Name)
|
||||||
case "perm.delete":
|
case "perm.delete":
|
||||||
defaults.Perm.Delete = mustGetBool(flags, flag.Name)
|
defaults.Perm.Delete, err = flags.GetBool(flag.Name)
|
||||||
case "perm.share":
|
case "perm.share":
|
||||||
defaults.Perm.Share = mustGetBool(flags, flag.Name)
|
defaults.Perm.Share, err = flags.GetBool(flag.Name)
|
||||||
case "perm.download":
|
case "perm.download":
|
||||||
defaults.Perm.Download = mustGetBool(flags, flag.Name)
|
defaults.Perm.Download, err = flags.GetBool(flag.Name)
|
||||||
case "commands":
|
case "commands":
|
||||||
commands, err := flags.GetStringSlice(flag.Name)
|
defaults.Commands, err = flags.GetStringSlice(flag.Name)
|
||||||
checkErr(err)
|
|
||||||
defaults.Commands = commands
|
|
||||||
case "sorting.by":
|
case "sorting.by":
|
||||||
defaults.Sorting.By = mustGetString(flags, flag.Name)
|
defaults.Sorting.By, err = flags.GetString(flag.Name)
|
||||||
case "sorting.asc":
|
case "sorting.asc":
|
||||||
defaults.Sorting.Asc = mustGetBool(flags, flag.Name)
|
defaults.Sorting.Asc, err = flags.GetBool(flag.Name)
|
||||||
|
case "dateFormat":
|
||||||
|
defaults.DateFormat, err = flags.GetBool(flag.Name)
|
||||||
|
case "hideDotfiles":
|
||||||
|
defaults.HideDotfiles, err = flags.GetBool(flag.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,4 +154,6 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
|
||||||
} else {
|
} else {
|
||||||
flags.Visit(visit)
|
flags.Visit(visit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,37 +15,68 @@ var usersAddCmd = &cobra.Command{
|
||||||
Use: "add <username> <password>",
|
Use: "add <username> <password>",
|
||||||
Short: "Create a new user",
|
Short: "Create a new user",
|
||||||
Long: `Create a new user and add it to the database.`,
|
Long: `Create a new user and add it to the database.`,
|
||||||
Args: cobra.ExactArgs(2), //nolint:mnd
|
Args: cobra.ExactArgs(2),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||||
s, err := d.store.Settings.Get()
|
flags := cmd.Flags()
|
||||||
checkErr(err)
|
s, err := st.Settings.Get()
|
||||||
getUserDefaults(cmd.Flags(), &s.Defaults, false)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = getUserDefaults(flags, &s.Defaults, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
password, err := users.HashPwd(args[1])
|
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
user := &users.User{
|
user := &users.User{
|
||||||
Username: args[0],
|
Username: args[0],
|
||||||
Password: password,
|
Password: password,
|
||||||
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
|
}
|
||||||
|
|
||||||
|
user.LockPassword, err = flags.GetBool("lockPassword")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.DateFormat, err = flags.GetBool("dateFormat")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Defaults.Apply(user)
|
s.Defaults.Apply(user)
|
||||||
|
|
||||||
servSettings, err := d.store.Settings.GetServer()
|
servSettings, err := st.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// since getUserDefaults() polluted s.Defaults.Scope
|
// since getUserDefaults() polluted s.Defaults.Scope
|
||||||
// which makes the Scope not the one saved in the db
|
// which makes the Scope not the one saved in the db
|
||||||
// we need the right s.Defaults.Scope here
|
// we need the right s.Defaults.Scope here
|
||||||
s2, err := d.store.Settings.Get()
|
s2, err := st.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
user.Scope = userHome
|
user.Scope = userHome
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
err = st.Users.Save(user)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printUsers([]*users.User{user})
|
printUsers([]*users.User{user})
|
||||||
}, pythonConfig{}),
|
return nil
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,16 @@ var usersExportCmd = &cobra.Command{
|
||||||
Long: `Export all users to a json or yaml file. Please indicate the
|
Long: `Export all users to a json or yaml file. Please indicate the
|
||||||
path to the file where you want to write the users.`,
|
path to the file where you want to write the users.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||||
list, err := d.store.Users.Gets("")
|
list, err := st.Users.Gets("", false)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = marshal(args[0], list)
|
err = marshal(args[0], list)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
}, pythonConfig{}),
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,17 @@ var usersFindCmd = &cobra.Command{
|
||||||
Short: "Find a user by username or id",
|
Short: "Find a user by username or id",
|
||||||
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
|
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: findUsers,
|
RunE: findUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersLsCmd = &cobra.Command{
|
var usersLsCmd = &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
Short: "List all users.",
|
Short: "List all users.",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: findUsers,
|
RunE: findUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
|
var findUsers = withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||||
var (
|
var (
|
||||||
list []*users.User
|
list []*users.User
|
||||||
user *users.User
|
user *users.User
|
||||||
|
|
@ -36,16 +36,19 @@ var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
if len(args) == 1 {
|
if len(args) == 1 {
|
||||||
username, id := parseUsernameOrID(args[0])
|
username, id := parseUsernameOrID(args[0])
|
||||||
if username != "" {
|
if username != "" {
|
||||||
user, err = d.store.Users.Get("", username)
|
user, err = st.Users.Get("", false, username)
|
||||||
} else {
|
} else {
|
||||||
user, err = d.store.Users.Get("", id)
|
user, err = st.Users.Get("", false, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
list = []*users.User{user}
|
list = []*users.User{user}
|
||||||
} else {
|
} else {
|
||||||
list, err = d.store.Users.Gets("")
|
list, err = st.Users.Gets("", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printUsers(list)
|
printUsers(list)
|
||||||
}, pythonConfig{})
|
return nil
|
||||||
|
}, storeOptions{})
|
||||||
|
|
|
||||||
|
|
@ -25,50 +25,71 @@ file. You can use this command to import new users to your
|
||||||
installation. For that, just don't place their ID on the files
|
installation. For that, just don't place their ID on the files
|
||||||
list or set it to 0.`,
|
list or set it to 0.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||||
|
flags := cmd.Flags()
|
||||||
fd, err := os.Open(args[0])
|
fd, err := os.Open(args[0])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
list := []*users.User{}
|
list := []*users.User{}
|
||||||
err = unmarshal(args[0], &list)
|
err = unmarshal(args[0], &list)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
for _, user := range list {
|
|
||||||
err = user.Clean("")
|
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if mustGetBool(cmd.Flags(), "replace") {
|
for _, user := range list {
|
||||||
oldUsers, err := d.store.Users.Gets("")
|
err = user.Clean("", false)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
err = marshal("users.backup.json", list)
|
|
||||||
checkErr(err)
|
|
||||||
|
|
||||||
for _, user := range oldUsers {
|
|
||||||
err = d.store.Users.Delete(user.ID)
|
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
overwrite := mustGetBool(cmd.Flags(), "overwrite")
|
replace, err := flags.GetBool("replace")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if replace {
|
||||||
|
oldUsers, userImportErr := st.Users.Gets("", false)
|
||||||
|
if userImportErr != nil {
|
||||||
|
return userImportErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = marshal("users.backup.json", list)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range oldUsers {
|
||||||
|
err = st.Users.Delete(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overwrite, err := flags.GetBool("overwrite")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, user := range list {
|
for _, user := range list {
|
||||||
onDB, err := d.store.Users.Get("", user.ID)
|
onDB, err := st.Users.Get("", false, user.ID)
|
||||||
|
|
||||||
// User exists in DB.
|
// User exists in DB.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if !overwrite {
|
if !overwrite {
|
||||||
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registred"))
|
return errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the usernames mismatch, check if there is another one in the DB
|
// If the usernames mismatch, check if there is another one in the DB
|
||||||
// with the new username. If there is, print an error and cancel the
|
// with the new username. If there is, print an error and cancel the
|
||||||
// operation
|
// operation
|
||||||
if user.Username != onDB.Username {
|
if user.Username != onDB.Username {
|
||||||
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:shadow
|
if conflictuous, err := st.Users.Get("", false, user.Username); err == nil {
|
||||||
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -77,13 +98,16 @@ list or set it to 0.`,
|
||||||
user.ID = 0
|
user.ID = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
err = st.Users.Save(user)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, pythonConfig{}),
|
return nil
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
func usernameConflictError(username string, originalID, newID uint) error {
|
func usernameConflictError(username string, originalID, newID uint) error {
|
||||||
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`,
|
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registered with the user %d`,
|
||||||
newID, username, originalID)
|
newID, username, originalID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,20 @@ var usersRmCmd = &cobra.Command{
|
||||||
Short: "Delete a user by username or id",
|
Short: "Delete a user by username or id",
|
||||||
Long: `Delete a user by username or id`,
|
Long: `Delete a user by username or id`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||||
username, id := parseUsernameOrID(args[0])
|
username, id := parseUsernameOrID(args[0])
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if username != "" {
|
if username != "" {
|
||||||
err = d.store.Users.Delete(username)
|
err = st.Users.Delete(username)
|
||||||
} else {
|
} else {
|
||||||
err = d.store.Users.Delete(id)
|
err = st.Users.Delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Println("user deleted successfully")
|
fmt.Println("user deleted successfully")
|
||||||
}, pythonConfig{}),
|
return nil
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,53 +21,91 @@ var usersUpdateCmd = &cobra.Command{
|
||||||
Long: `Updates an existing user. Set the flags for the
|
Long: `Updates an existing user. Set the flags for the
|
||||||
options you want to change.`,
|
options you want to change.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||||
username, id := parseUsernameOrID(args[0])
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
password := mustGetString(flags, "password")
|
username, id := parseUsernameOrID(args[0])
|
||||||
newUsername := mustGetString(flags, "username")
|
password, err := flags.GetString("password")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newUsername, err := flags.GetString("username")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := st.Settings.Get()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
|
||||||
user *users.User
|
user *users.User
|
||||||
)
|
)
|
||||||
|
|
||||||
if id != 0 {
|
if id != 0 {
|
||||||
user, err = d.store.Users.Get("", id)
|
user, err = st.Users.Get("", false, id)
|
||||||
} else {
|
} else {
|
||||||
user, err = d.store.Users.Get("", username)
|
user, err = st.Users.Get("", false, username)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
|
||||||
|
|
||||||
defaults := settings.UserDefaults{
|
defaults := settings.UserDefaults{
|
||||||
Scope: user.Scope,
|
Scope: user.Scope,
|
||||||
Locale: user.Locale,
|
Locale: user.Locale,
|
||||||
ViewMode: user.ViewMode,
|
ViewMode: user.ViewMode,
|
||||||
Perm: user.Perm,
|
SingleClick: user.SingleClick,
|
||||||
Sorting: user.Sorting,
|
RedirectAfterCopyMove: user.RedirectAfterCopyMove,
|
||||||
Commands: user.Commands,
|
Perm: user.Perm,
|
||||||
|
Sorting: user.Sorting,
|
||||||
|
Commands: user.Commands,
|
||||||
}
|
}
|
||||||
getUserDefaults(flags, &defaults, false)
|
|
||||||
|
err = getUserDefaults(flags, &defaults, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
user.Scope = defaults.Scope
|
user.Scope = defaults.Scope
|
||||||
user.Locale = defaults.Locale
|
user.Locale = defaults.Locale
|
||||||
user.ViewMode = defaults.ViewMode
|
user.ViewMode = defaults.ViewMode
|
||||||
|
user.SingleClick = defaults.SingleClick
|
||||||
|
user.RedirectAfterCopyMove = defaults.RedirectAfterCopyMove
|
||||||
user.Perm = defaults.Perm
|
user.Perm = defaults.Perm
|
||||||
user.Commands = defaults.Commands
|
user.Commands = defaults.Commands
|
||||||
user.Sorting = defaults.Sorting
|
user.Sorting = defaults.Sorting
|
||||||
user.LockPassword = mustGetBool(flags, "lockPassword")
|
user.LockPassword, err = flags.GetBool("lockPassword")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.DateFormat, err = flags.GetBool("dateFormat")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if newUsername != "" {
|
if newUsername != "" {
|
||||||
user.Username = newUsername
|
user.Username = newUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
if password != "" {
|
if password != "" {
|
||||||
user.Password, err = users.HashPwd(password)
|
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Users.Update(user)
|
err = st.Users.Update(user)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printUsers([]*users.User{user})
|
printUsers([]*users.User{user})
|
||||||
}, pythonConfig{}),
|
return nil
|
||||||
|
}, storeOptions{}),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
242
cmd/utils.go
242
cmd/utils.go
|
|
@ -3,64 +3,50 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/asdine/storm"
|
"github.com/asdine/storm/v3"
|
||||||
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
yaml "gopkg.in/yaml.v2"
|
"github.com/spf13/viper"
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkErr(err error) {
|
const databasePermissions = 0640
|
||||||
|
|
||||||
|
func getAndParseFileMode(flags *pflag.FlagSet, name string) (fs.FileMode, error) {
|
||||||
|
mode, err := flags.GetString(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func mustGetString(flags *pflag.FlagSet, flag string) string {
|
b, err := strconv.ParseUint(mode, 0, 32)
|
||||||
s, err := flags.GetString(flag)
|
if err != nil {
|
||||||
checkErr(err)
|
return 0, err
|
||||||
return s
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
|
return fs.FileMode(b), nil
|
||||||
b, err := flags.GetBool(flag)
|
|
||||||
checkErr(err)
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
|
|
||||||
b, err := flags.GetUint(flag)
|
|
||||||
checkErr(err)
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateKey() []byte {
|
func generateKey() []byte {
|
||||||
k, err := settings.GenerateKey()
|
k, err := settings.GenerateKey()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
type cobraFunc func(cmd *cobra.Command, args []string)
|
|
||||||
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
|
|
||||||
|
|
||||||
type pythonConfig struct {
|
|
||||||
noDB bool
|
|
||||||
allowNoDB bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type pythonData struct {
|
|
||||||
hadDB bool
|
|
||||||
store *storage.Storage
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbExists(path string) (bool, error) {
|
func dbExists(path string) (bool, error) {
|
||||||
stat, err := os.Stat(path)
|
stat, err := os.Stat(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -71,7 +57,7 @@ func dbExists(path string) (bool, error) {
|
||||||
d := filepath.Dir(path)
|
d := filepath.Dir(path)
|
||||||
_, err = os.Stat(d)
|
_, err = os.Stat(d)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:shadow
|
if err := os.MkdirAll(d, 0700); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
@ -81,34 +67,143 @@ func dbExists(path string) (bool, error) {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
// Generate the replacements for all environment variables. This allows to
|
||||||
return func(cmd *cobra.Command, args []string) {
|
// use FB_BRANDING_DISABLE_EXTERNAL environment variables, even when the
|
||||||
data := pythonData{hadDB: true}
|
// option name is branding.disableExternal.
|
||||||
|
func generateEnvKeyReplacements(cmd *cobra.Command) []string {
|
||||||
|
replacements := []string{}
|
||||||
|
|
||||||
path := getParam(cmd.Flags(), "database")
|
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||||
exists, err := dbExists(path)
|
oldName := strings.ToUpper(f.Name)
|
||||||
|
newName := strings.ToUpper(lo.SnakeCase(f.Name))
|
||||||
|
replacements = append(replacements, oldName, newName)
|
||||||
|
})
|
||||||
|
|
||||||
|
return replacements
|
||||||
|
}
|
||||||
|
|
||||||
|
func initViper(cmd *cobra.Command) (*viper.Viper, error) {
|
||||||
|
v := viper.New()
|
||||||
|
|
||||||
|
// Get config file from flag
|
||||||
|
cfgFile, err := cmd.Flags().GetString("config")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration file
|
||||||
|
if cfgFile == "" {
|
||||||
|
home, err := homedir.Dir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
} else if exists && cfg.noDB {
|
}
|
||||||
log.Fatal(path + " already exists")
|
v.AddConfigPath(".")
|
||||||
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
|
v.AddConfigPath(home)
|
||||||
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
|
v.AddConfigPath("/etc/filebrowser/")
|
||||||
|
v.SetConfigName(".filebrowser")
|
||||||
|
} else {
|
||||||
|
v.SetConfigFile(cfgFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
v.SetEnvPrefix("FB")
|
||||||
|
v.AutomaticEnv()
|
||||||
|
v.SetEnvKeyReplacer(strings.NewReplacer(generateEnvKeyReplacements(cmd)...))
|
||||||
|
|
||||||
|
// Bind the flags
|
||||||
|
err = v.BindPFlags(cmd.Flags())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read in configuration
|
||||||
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
|
||||||
|
if errors.As(err, &viper.ConfigParseError{}) {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data.hadDB = exists
|
log.Println("No config file used")
|
||||||
db, err := storm.Open(path)
|
} else {
|
||||||
checkErr(err)
|
log.Printf("Using config file: %s", v.ConfigFileUsed())
|
||||||
defer db.Close()
|
|
||||||
data.store, err = bolt.NewStorage(db)
|
|
||||||
checkErr(err)
|
|
||||||
fn(cmd, args, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return Viper
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type store struct {
|
||||||
|
*storage.Storage
|
||||||
|
databaseExisted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type storeOptions struct {
|
||||||
|
expectsNoDatabase bool
|
||||||
|
allowsNoDatabase bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type cobraFunc func(cmd *cobra.Command, args []string) error
|
||||||
|
|
||||||
|
// withViperAndStore initializes Viper and the storage.Store and passes them to the callback function.
|
||||||
|
// This function should only be used by [withStore] and the root command. No other command should call
|
||||||
|
// this function directly.
|
||||||
|
func withViperAndStore(fn func(cmd *cobra.Command, args []string, v *viper.Viper, store *store) error, options storeOptions) cobraFunc {
|
||||||
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
v, err := initViper(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := filepath.Abs(v.GetString("database"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := dbExists(path)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
case exists && options.expectsNoDatabase:
|
||||||
|
log.Fatal(path + " already exists")
|
||||||
|
case !exists && !options.expectsNoDatabase && !options.allowsNoDatabase:
|
||||||
|
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
|
||||||
|
case !exists && !options.expectsNoDatabase:
|
||||||
|
log.Println("WARNING: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(path, "filebrowser.db"))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Using database: " + path)
|
||||||
|
|
||||||
|
db, err := storm.Open(path, storm.BoltOptions(databasePermissions, nil))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
storage, err := bolt.NewStorage(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
store := &store{
|
||||||
|
Storage: storage,
|
||||||
|
databaseExisted: exists,
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(cmd, args, v, store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withStore(fn func(cmd *cobra.Command, args []string, store *store) error, options storeOptions) cobraFunc {
|
||||||
|
return withViperAndStore(func(cmd *cobra.Command, args []string, _ *viper.Viper, store *store) error {
|
||||||
|
return fn(cmd, args, store)
|
||||||
|
}, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshal(filename string, data interface{}) error {
|
func marshal(filename string, data interface{}) error {
|
||||||
fd, err := os.Create(filename)
|
fd, err := os.Create(filename)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
switch ext := filepath.Ext(filename); ext {
|
switch ext := filepath.Ext(filename); ext {
|
||||||
|
|
@ -116,7 +211,7 @@ func marshal(filename string, data interface{}) error {
|
||||||
encoder := json.NewEncoder(fd)
|
encoder := json.NewEncoder(fd)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
return encoder.Encode(data)
|
return encoder.Encode(data)
|
||||||
case ".yml", ".yaml": //nolint:goconst
|
case ".yml", ".yaml":
|
||||||
encoder := yaml.NewEncoder(fd)
|
encoder := yaml.NewEncoder(fd)
|
||||||
return encoder.Encode(data)
|
return encoder.Encode(data)
|
||||||
default:
|
default:
|
||||||
|
|
@ -126,7 +221,9 @@ func marshal(filename string, data interface{}) error {
|
||||||
|
|
||||||
func unmarshal(filename string, data interface{}) error {
|
func unmarshal(filename string, data interface{}) error {
|
||||||
fd, err := os.Open(filename)
|
fd, err := os.Open(filename)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
switch ext := filepath.Ext(filename); ext {
|
switch ext := filepath.Ext(filename); ext {
|
||||||
|
|
@ -152,29 +249,14 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
|
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
|
||||||
result := make(map[string]interface{})
|
// then returns empty string array, else returns the split word array of cmd.
|
||||||
for k, v := range in {
|
// This is to ensure the result will never be []string{""}
|
||||||
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
|
func convertCmdStrToCmdArray(cmd string) []string {
|
||||||
}
|
var cmdArray []string
|
||||||
return result
|
trimmedCmdStr := strings.TrimSpace(cmd)
|
||||||
}
|
if trimmedCmdStr != "" {
|
||||||
|
cmdArray = strings.Split(trimmedCmdStr, " ")
|
||||||
func cleanUpInterfaceArray(in []interface{}) []interface{} {
|
|
||||||
result := make([]interface{}, len(in))
|
|
||||||
for i, v := range in {
|
|
||||||
result[i] = cleanUpMapValue(v)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanUpMapValue(v interface{}) interface{} {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
return cleanUpInterfaceArray(v)
|
|
||||||
case map[interface{}]interface{}:
|
|
||||||
return cleanUpInterfaceMap(v)
|
|
||||||
default:
|
|
||||||
return v
|
|
||||||
}
|
}
|
||||||
|
return cmdArray
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ func init() {
|
||||||
var versionCmd = &cobra.Command{
|
var versionCmd = &cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Print the version number",
|
Short: "Print the version number",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
|
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
compose.yaml
Normal file
32
compose.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
services:
|
||||||
|
filebrowser:
|
||||||
|
container_name: filebrowser
|
||||||
|
image: filebrowser/filebrowser:latest
|
||||||
|
networks:
|
||||||
|
- filebrowser
|
||||||
|
ports:
|
||||||
|
- 8000:80
|
||||||
|
volumes:
|
||||||
|
- filebrowser:/flux/vault
|
||||||
|
environment:
|
||||||
|
- FB_REDIS_CACHE_URL=redis://default:filebrowser@redis:6379 # Use rediss:// for ssl
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: redis
|
||||||
|
image: redis:latest
|
||||||
|
networks:
|
||||||
|
- filebrowser
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
cat > /tmp/users.acl <<EOF
|
||||||
|
user default on >filebrowser ~* +@all
|
||||||
|
EOF
|
||||||
|
redis-server --aclfile /tmp/users.acl
|
||||||
|
|
||||||
|
networks:
|
||||||
|
filebrowser:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
filebrowser:
|
||||||
11
diskcache/cache.go
Normal file
11
diskcache/cache.go
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Interface interface {
|
||||||
|
Store(ctx context.Context, key string, value []byte) error
|
||||||
|
Load(ctx context.Context, key string) (value []byte, exist bool, err error)
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
}
|
||||||
110
diskcache/file_cache.go
Normal file
110
diskcache/file_cache.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileCache struct {
|
||||||
|
fs afero.Fs
|
||||||
|
|
||||||
|
// granular locks
|
||||||
|
scopedLocks struct {
|
||||||
|
sync.Mutex
|
||||||
|
sync.Once
|
||||||
|
locks map[string]sync.Locker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(fs afero.Fs, root string) *FileCache {
|
||||||
|
return &FileCache{
|
||||||
|
fs: afero.NewBasePathFs(fs, root),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
|
||||||
|
mu := f.getScopedLocks(key)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
fileName := f.getFileName(key)
|
||||||
|
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) Load(_ context.Context, key string) (value []byte, exist bool, err error) {
|
||||||
|
r, ok, err := f.open(key)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return nil, ok, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
value, err = io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return value, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) Delete(_ context.Context, key string) error {
|
||||||
|
mu := f.getScopedLocks(key)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
fileName := f.getFileName(key)
|
||||||
|
if err := f.fs.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) open(key string) (afero.File, bool, error) {
|
||||||
|
fileName := f.getFileName(key)
|
||||||
|
file, err := f.fs.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getScopedLocks pull lock from the map if found or create a new one
|
||||||
|
func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
|
||||||
|
f.scopedLocks.Do(func() { f.scopedLocks.locks = map[string]sync.Locker{} })
|
||||||
|
|
||||||
|
f.scopedLocks.Lock()
|
||||||
|
lock, ok := f.scopedLocks.locks[key]
|
||||||
|
if !ok {
|
||||||
|
lock = &sync.Mutex{}
|
||||||
|
f.scopedLocks.locks[key] = lock
|
||||||
|
}
|
||||||
|
f.scopedLocks.Unlock()
|
||||||
|
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) getFileName(key string) string {
|
||||||
|
hasher := sha1.New()
|
||||||
|
_, _ = hasher.Write([]byte(key))
|
||||||
|
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
|
||||||
|
}
|
||||||
55
diskcache/file_cache_test.go
Normal file
55
diskcache/file_cache_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileCache(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
const (
|
||||||
|
key = "key"
|
||||||
|
value = "some text"
|
||||||
|
newValue = "new text"
|
||||||
|
cacheRoot = "/cache"
|
||||||
|
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
|
||||||
|
)
|
||||||
|
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
cache := New(fs, "/cache")
|
||||||
|
|
||||||
|
// store new key
|
||||||
|
err := cache.Store(ctx, key, []byte(value))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
|
||||||
|
|
||||||
|
// update existing key
|
||||||
|
err = cache.Store(ctx, key, []byte(newValue))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
|
||||||
|
|
||||||
|
// delete key
|
||||||
|
err = cache.Delete(ctx, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
exists, err := afero.Exists(fs, filepath.Join(cacheRoot, cachedFilePath))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkValue(ctx context.Context, t *testing.T, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) {
|
||||||
|
t.Helper()
|
||||||
|
// check actual file content
|
||||||
|
b, err := afero.ReadFile(fs, fileFullPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, wantValue, string(b))
|
||||||
|
|
||||||
|
// check cache content
|
||||||
|
b, ok, err := cache.Load(ctx, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, wantValue, string(b))
|
||||||
|
}
|
||||||
24
diskcache/noop_cache.go
Normal file
24
diskcache/noop_cache.go
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NoOp struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNoOp() *NoOp {
|
||||||
|
return &NoOp{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoOp) Store(_ context.Context, _ string, _ []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoOp) Load(_ context.Context, _ string) (value []byte, exist bool, err error) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoOp) Delete(_ context.Context, _ string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
9
docker/alpine/healthcheck.sh
Normal file
9
docker/alpine/healthcheck.sh
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PORT=${FB_PORT:-$(cat /config/settings.json | sh /JSON.sh | grep '\["port"\]' | awk '{print $2}')}
|
||||||
|
ADDRESS=${FB_ADDRESS:-$(cat /config/settings.json | sh /JSON.sh | grep '\["address"\]' | awk '{print $2}' | sed 's/"//g')}
|
||||||
|
ADDRESS=${ADDRESS:-localhost}
|
||||||
|
|
||||||
|
wget -q --spider http://$ADDRESS:$PORT/health || exit 1
|
||||||
35
docker/alpine/init.sh
Executable file
35
docker/alpine/init.sh
Executable file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Ensure configuration exists
|
||||||
|
if [ ! -f "/config/settings.json" ]; then
|
||||||
|
cp -a /defaults/settings.json /config/settings.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract config file path from arguments
|
||||||
|
config_file=""
|
||||||
|
next_is_config=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$next_is_config" -eq 1 ]; then
|
||||||
|
config_file="$arg"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-c|--config)
|
||||||
|
next_is_config=1
|
||||||
|
;;
|
||||||
|
-c=*|--config=*)
|
||||||
|
config_file="${arg#*=}"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# If no config argument is provided, set the default and add it to the args
|
||||||
|
if [ -z "$config_file" ]; then
|
||||||
|
config_file="/config/settings.json"
|
||||||
|
set -- --config=/config/settings.json "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec filebrowser "$@"
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
"baseURL": "",
|
"baseURL": "",
|
||||||
"address": "",
|
"address": "",
|
||||||
"log": "stdout",
|
"log": "stdout",
|
||||||
"database": "/database.db",
|
"database": "/database/filebrowser.db",
|
||||||
"root": "/srv"
|
"root": "/srv"
|
||||||
}
|
}
|
||||||
9
docker/common/healthcheck.sh
Executable file
9
docker/common/healthcheck.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PORT=${FB_PORT:-$(jq -r .port /config/settings.json)}
|
||||||
|
ADDRESS=${FB_ADDRESS:-$(jq -r .address /config/settings.json)}
|
||||||
|
ADDRESS=${ADDRESS:-localhost}
|
||||||
|
|
||||||
|
wget -q --spider http://$ADDRESS:$PORT/health || exit 1
|
||||||
12
docker/s6/custom-cont-init.d/20-config
Executable file
12
docker/s6/custom-cont-init.d/20-config
Executable file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
|
# Ensure configuration exists
|
||||||
|
if [ ! -f "/config/settings.json" ]; then
|
||||||
|
cp -a /defaults/settings.json /config/settings.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
# permissions
|
||||||
|
chown abc:abc \
|
||||||
|
/config/settings.json \
|
||||||
|
/database \
|
||||||
|
/srv
|
||||||
3
docker/s6/etc/services.d/filebrowser/run
Executable file
3
docker/s6/etc/services.d/filebrowser/run
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
|
exec s6-setuidgid abc filebrowser -c /config/settings.json;
|
||||||
49
docs/README.md
Normal file
49
docs/README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<p align="center">
|
||||||
|
<img src="../branding/banner.png" width="550"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> **File Browser is archived on 2026-09-01.** There will be no further releases and no security fixes. Existing releases and Docker images stay online. For the known unaddressed security issues and hardening guidance, read the [README](../README.md#security).
|
||||||
|
|
||||||
|
File Browser provides a file managing interface within a specified directory and it can be used to upload, delete, preview and edit your files. It is a **create-your-own-cloud**-kind of software where you can just install it on your server, direct it to a path and access your files through a nice web interface.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Installation](installation.md)
|
||||||
|
- [Customization](customization.md)
|
||||||
|
- [Authentication](authentication.md)
|
||||||
|
- [Command Execution](command-execution.md)
|
||||||
|
- [Deployment](deployment.md)
|
||||||
|
- [Troubleshooting](troubleshooting.md)
|
||||||
|
- [Command Line Usage](cli/filebrowser.md)
|
||||||
|
|
||||||
|
Project-level documents live in the repository root: [README](../README.md), [Building File Browser](../CONTRIBUTING.md), [Security Policy](../SECURITY.md), [Code of Conduct](../CODE-OF-CONDUCT.md), [Changelog](../CHANGELOG.md) and [License](../LICENSE).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Easy Login System**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Sleek Interface**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **User Management**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **File Editing**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Custom Commands**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Customization**
|
||||||
|
|
||||||
|

|
||||||
167
docs/authentication.md
Normal file
167
docs/authentication.md
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
# Authentication
|
||||||
|
|
||||||
|
There are three possible authentication methods. Each one of them has its own capabilities and specification. Adding another authentication method is described in [Building File Browser](../CONTRIBUTING.md#authentication-provider).
|
||||||
|
|
||||||
|
## JSON Auth (default)
|
||||||
|
|
||||||
|
We call it JSON Authentication but it is just the default authentication method and the one that is provided by default if you don't make any changes. It is set by default, but if you've made changes before you can revert to using JSON auth:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
filebrowser config set --auth.method=json
|
||||||
|
```
|
||||||
|
|
||||||
|
This method can also be extended with **reCAPTCHA** verification during login:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
filebrowser config set --auth.method=json \
|
||||||
|
--recaptcha.key site-key \
|
||||||
|
--recaptcha.secret private-key
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, we use [Google's reCAPTCHA](https://developers.google.com/recaptcha/docs/display) service. If you live in China, or want to use other provider, you can change the host with the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
filebrowser config set --recaptcha.host https://recaptcha.net
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `https://recaptcha.net` is any provider you want.
|
||||||
|
|
||||||
|
## Proxy Header
|
||||||
|
|
||||||
|
If you have a reverse proxy you want to use to login your users, you do it via our `proxy` authentication method. To configure this method, your proxy must send an HTTP header containing the username of the logged in user:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
filebrowser config set --auth.method=proxy --auth.header=X-My-Header
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `X-My-Header` is the HTTP header provided by your proxy with the username.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> File Browser will blindly trust the provided header. If the proxy can be bypassed, an attacker could simply attach the header and get admin access. Please ensure that File Browser is not accessible from untrusted networks, and that the proxy is correctly configured to strip/overwrite the header from client requests.
|
||||||
|
|
||||||
|
## Hook Authentication
|
||||||
|
|
||||||
|
The Hook Authentication method in FileBrowser allows developers to delegate user authentication to an external script or program. Instead of validating credentials internally, FileBrowser sends the username and password to a custom command defined by the administrator. This command receives the credentials through environment variables and returns key‑value pairs indicating whether the user should be authenticated, blocked, or passed through.
|
||||||
|
|
||||||
|
The hook’s output controls user permissions, scope, locale, and other attributes, making it a powerful and extensible authentication mechanism.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> The submitted username and password are attacker-controlled and are handed to your hook command as the `USERNAME` and `PASSWORD` environment variables. File Browser runs the command directly, without a shell, so the values themselves are inert. However, your script must treat them as untrusted: always quote them (`"$USERNAME"`, `"$PASSWORD"`) and never pass them unquoted to a shell, `eval`, `bash -c`, command substitution, or backticks. A hook script that shell-evaluates these values turns any login request into remote code execution.
|
||||||
|
|
||||||
|
For example, the following code delegates filebrowser authentication to a PowerShell script on Windows. You can configure any command (for example, a script in Python, Node.js, etc.).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
filebrowser config set --auth.method=hook --auth.command="powershell.exe -File C:\route\to\your\script\auth.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the code for the auth.ps1 script
|
||||||
|
|
||||||
|
```sh
|
||||||
|
param()
|
||||||
|
|
||||||
|
# Get FileBrowser credentials from environment variables
|
||||||
|
$username = $env:USERNAME
|
||||||
|
$password = $env:PASSWORD
|
||||||
|
|
||||||
|
# Users dictionary (for testing purposes only)
|
||||||
|
$users = @{
|
||||||
|
"admin" = "kideW48v7-SdE*"
|
||||||
|
"test" = "2sDd3-etrytñK"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the user exists in the dictionary and verify the password
|
||||||
|
if ($users.ContainsKey($username) -and $users[$username] -eq $password) {
|
||||||
|
|
||||||
|
# Successful authentication
|
||||||
|
Write-Output "hook.action=auth" # Hook action (in this case, "auth", is required for successful authentication)
|
||||||
|
|
||||||
|
Write-Output "user.perm.admin=true" # Set admin role (all permissions)
|
||||||
|
#You can also define specific permissions like this:
|
||||||
|
Write-Output "user.perm.execute=true"
|
||||||
|
Write-Output "user.perm.create=true"
|
||||||
|
Write-Output "user.perm.rename=true"
|
||||||
|
Write-Output "user.perm.modify=true"
|
||||||
|
Write-Output "user.perm.delete=true"
|
||||||
|
Write-Output "user.perm.share=true"
|
||||||
|
Write-Output "user.perm.download=true"
|
||||||
|
|
||||||
|
Write-Output "user.locale=es" # Set language
|
||||||
|
Write-Output "user.viewMode=list" # Set view mode
|
||||||
|
Write-Output "user.scope=/" # Set FileBrowser scope
|
||||||
|
Write-Output "user.singleClick=true" # Set single click user configuration
|
||||||
|
Write-Output "user.hideDotfiles=false" # Set hide dot files user configuration
|
||||||
|
|
||||||
|
#Set other configuration
|
||||||
|
} else {
|
||||||
|
# Block authentication
|
||||||
|
Write-Output "hook.action=block"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook Output Format
|
||||||
|
|
||||||
|
A hook authentication script must output a series of key–value pairs, one per line, using the format:
|
||||||
|
|
||||||
|
```
|
||||||
|
key=value
|
||||||
|
```
|
||||||
|
|
||||||
|
FileBrowser reads these lines and applies the corresponding authentication action and user configuration.
|
||||||
|
|
||||||
|
#### Required Fields
|
||||||
|
|
||||||
|
The hook must output one of the following actions:
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
|--------|------------ |
|
||||||
|
| hook.action=auth | Authenticates the user. FileBrowser will create or update the user if needed. |
|
||||||
|
| hook.action=block | Rejects authentication. The login attempt fails. |
|
||||||
|
| hook.action=pass | Delegates authentication to FileBrowser’s internal password validation. |
|
||||||
|
|
||||||
|
For most custom authentication flows, auth or block are used.
|
||||||
|
|
||||||
|
Example of a successful authentication:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
hook.action=auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Optional User Fields
|
||||||
|
|
||||||
|
When `hook.action=auth` is returned, the hook may also define additional user attributes. These fields override FileBrowser defaults and allow full customization of the authenticated user.
|
||||||
|
|
||||||
|
1. Permissions
|
||||||
|
```
|
||||||
|
user.perm.admin=true
|
||||||
|
user.perm.execute=true
|
||||||
|
user.perm.create=true
|
||||||
|
user.perm.rename=true
|
||||||
|
user.perm.modify=true
|
||||||
|
user.perm.delete=true
|
||||||
|
user.perm.share=true
|
||||||
|
user.perm.download=true
|
||||||
|
```
|
||||||
|
> Setting user.perm.admin=true automatically enables all permissions.
|
||||||
|
|
||||||
|
2. User Interface and Behavior
|
||||||
|
```
|
||||||
|
user.locale=es
|
||||||
|
user.viewMode=list
|
||||||
|
user.singleClick=true
|
||||||
|
user.hideDotfiles=false
|
||||||
|
```
|
||||||
|
|
||||||
|
3. User Scope
|
||||||
|
```
|
||||||
|
user.scope=/
|
||||||
|
```
|
||||||
|
|
||||||
|
## No Authentication
|
||||||
|
|
||||||
|
We also provide a no authentication mechanism for users that want to use File Browser privately such in a home network. By setting this authentication method, the user with **id 1** will be used as the default users. Creating more users won't have any effect.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
filebrowser config set --auth.method=noauth
|
||||||
|
```
|
||||||
29
docs/cli/filebrowser-cmds-add.md
Normal file
29
docs/cli/filebrowser-cmds-add.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# filebrowser cmds add
|
||||||
|
|
||||||
|
Add a command to run on a specific event
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Add a command to run on a specific event.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser cmds add <event> <command> [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for add
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser cmds](filebrowser-cmds.md) - Command runner management utility
|
||||||
|
|
||||||
30
docs/cli/filebrowser-cmds-ls.md
Normal file
30
docs/cli/filebrowser-cmds-ls.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# filebrowser cmds ls
|
||||||
|
|
||||||
|
List all commands for each event
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
List all commands for each event.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser cmds ls [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-e, --event string event name, without 'before' or 'after'
|
||||||
|
-h, --help help for ls
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser cmds](filebrowser-cmds.md) - Command runner management utility
|
||||||
|
|
||||||
37
docs/cli/filebrowser-cmds-rm.md
Normal file
37
docs/cli/filebrowser-cmds-rm.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# filebrowser cmds rm
|
||||||
|
|
||||||
|
Removes a command from an event hooker
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Removes a command from an event hooker. The provided index
|
||||||
|
is the same that's printed when you run 'cmds ls'. Note
|
||||||
|
that after each removal/addition, the index of the
|
||||||
|
commands change. So be careful when removing them after each
|
||||||
|
other.
|
||||||
|
|
||||||
|
You can also specify an optional parameter (index_end) so
|
||||||
|
you can remove all commands from 'index' to 'index_end',
|
||||||
|
including 'index_end'.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser cmds rm <event> <index> [index_end] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for rm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser cmds](filebrowser-cmds.md) - Command runner management utility
|
||||||
|
|
||||||
28
docs/cli/filebrowser-cmds.md
Normal file
28
docs/cli/filebrowser-cmds.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# filebrowser cmds
|
||||||
|
|
||||||
|
Command runner management utility
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Command runner management utility.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for cmds
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser](filebrowser.md) - A stylish web-based file browser
|
||||||
|
* [filebrowser cmds add](filebrowser-cmds-add.md) - Add a command to run on a specific event
|
||||||
|
* [filebrowser cmds ls](filebrowser-cmds-ls.md) - List all commands for each event
|
||||||
|
* [filebrowser cmds rm](filebrowser-cmds-rm.md) - Removes a command from an event hooker
|
||||||
|
|
||||||
50
docs/cli/filebrowser-completion-bash.md
Normal file
50
docs/cli/filebrowser-completion-bash.md
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# filebrowser completion bash
|
||||||
|
|
||||||
|
Generate the autocompletion script for bash
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for the bash shell.
|
||||||
|
|
||||||
|
This script depends on the 'bash-completion' package.
|
||||||
|
If it is not installed already, you can install it via your OS's package manager.
|
||||||
|
|
||||||
|
To load completions in your current shell session:
|
||||||
|
|
||||||
|
source <(filebrowser completion bash)
|
||||||
|
|
||||||
|
To load completions for every new session, execute once:
|
||||||
|
|
||||||
|
### Linux:
|
||||||
|
|
||||||
|
filebrowser completion bash > /etc/bash_completion.d/filebrowser
|
||||||
|
|
||||||
|
### macOS:
|
||||||
|
|
||||||
|
filebrowser completion bash > $(brew --prefix)/etc/bash_completion.d/filebrowser
|
||||||
|
|
||||||
|
You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser completion bash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for bash
|
||||||
|
--no-descriptions disable completion descriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser completion](filebrowser-completion.md) - Generate the autocompletion script for the specified shell
|
||||||
|
|
||||||
41
docs/cli/filebrowser-completion-fish.md
Normal file
41
docs/cli/filebrowser-completion-fish.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# filebrowser completion fish
|
||||||
|
|
||||||
|
Generate the autocompletion script for fish
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for the fish shell.
|
||||||
|
|
||||||
|
To load completions in your current shell session:
|
||||||
|
|
||||||
|
filebrowser completion fish | source
|
||||||
|
|
||||||
|
To load completions for every new session, execute once:
|
||||||
|
|
||||||
|
filebrowser completion fish > ~/.config/fish/completions/filebrowser.fish
|
||||||
|
|
||||||
|
You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser completion fish [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for fish
|
||||||
|
--no-descriptions disable completion descriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser completion](filebrowser-completion.md) - Generate the autocompletion script for the specified shell
|
||||||
|
|
||||||
38
docs/cli/filebrowser-completion-powershell.md
Normal file
38
docs/cli/filebrowser-completion-powershell.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# filebrowser completion powershell
|
||||||
|
|
||||||
|
Generate the autocompletion script for powershell
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for powershell.
|
||||||
|
|
||||||
|
To load completions in your current shell session:
|
||||||
|
|
||||||
|
filebrowser completion powershell | Out-String | Invoke-Expression
|
||||||
|
|
||||||
|
To load completions for every new session, add the output of the above command
|
||||||
|
to your powershell profile.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser completion powershell [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for powershell
|
||||||
|
--no-descriptions disable completion descriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser completion](filebrowser-completion.md) - Generate the autocompletion script for the specified shell
|
||||||
|
|
||||||
52
docs/cli/filebrowser-completion-zsh.md
Normal file
52
docs/cli/filebrowser-completion-zsh.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# filebrowser completion zsh
|
||||||
|
|
||||||
|
Generate the autocompletion script for zsh
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for the zsh shell.
|
||||||
|
|
||||||
|
If shell completion is not already enabled in your environment you will need
|
||||||
|
to enable it. You can execute the following once:
|
||||||
|
|
||||||
|
echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||||
|
|
||||||
|
To load completions in your current shell session:
|
||||||
|
|
||||||
|
source <(filebrowser completion zsh)
|
||||||
|
|
||||||
|
To load completions for every new session, execute once:
|
||||||
|
|
||||||
|
### Linux:
|
||||||
|
|
||||||
|
filebrowser completion zsh > "${fpath[1]}/_filebrowser"
|
||||||
|
|
||||||
|
### macOS:
|
||||||
|
|
||||||
|
filebrowser completion zsh > $(brew --prefix)/share/zsh/site-functions/_filebrowser
|
||||||
|
|
||||||
|
You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser completion zsh [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for zsh
|
||||||
|
--no-descriptions disable completion descriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser completion](filebrowser-completion.md) - Generate the autocompletion script for the specified shell
|
||||||
|
|
||||||
31
docs/cli/filebrowser-completion.md
Normal file
31
docs/cli/filebrowser-completion.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# filebrowser completion
|
||||||
|
|
||||||
|
Generate the autocompletion script for the specified shell
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Generate the autocompletion script for filebrowser for the specified shell.
|
||||||
|
See each sub-command's help for details on how to use the generated script.
|
||||||
|
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for completion
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser](filebrowser.md) - A stylish web-based file browser
|
||||||
|
* [filebrowser completion bash](filebrowser-completion-bash.md) - Generate the autocompletion script for bash
|
||||||
|
* [filebrowser completion fish](filebrowser-completion-fish.md) - Generate the autocompletion script for fish
|
||||||
|
* [filebrowser completion powershell](filebrowser-completion-powershell.md) - Generate the autocompletion script for powershell
|
||||||
|
* [filebrowser completion zsh](filebrowser-completion-zsh.md) - Generate the autocompletion script for zsh
|
||||||
|
|
||||||
29
docs/cli/filebrowser-config-cat.md
Normal file
29
docs/cli/filebrowser-config-cat.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# filebrowser config cat
|
||||||
|
|
||||||
|
Prints the configuration
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Prints the configuration.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser config cat [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for cat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser config](filebrowser-config.md) - Configuration management utility
|
||||||
|
|
||||||
31
docs/cli/filebrowser-config-export.md
Normal file
31
docs/cli/filebrowser-config-export.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# filebrowser config export
|
||||||
|
|
||||||
|
Export the configuration to a file
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Export the configuration to a file. The path must be for a
|
||||||
|
json or yaml file. This exported configuration can be changed,
|
||||||
|
and imported again with 'config import' command.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser config export <path> [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for export
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser config](filebrowser-config.md) - Configuration management utility
|
||||||
|
|
||||||
36
docs/cli/filebrowser-config-import.md
Normal file
36
docs/cli/filebrowser-config-import.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# filebrowser config import
|
||||||
|
|
||||||
|
Import a configuration file
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Import a configuration file. This will replace all the existing
|
||||||
|
configuration. Can be used with or without unexisting databases.
|
||||||
|
|
||||||
|
If used with a nonexisting database, a key will be generated
|
||||||
|
automatically. Otherwise the key will be kept the same as in the
|
||||||
|
database.
|
||||||
|
|
||||||
|
The path must be for a json or yaml file.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser config import <path> [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for import
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser config](filebrowser-config.md) - Configuration management utility
|
||||||
|
|
||||||
90
docs/cli/filebrowser-config-init.md
Normal file
90
docs/cli/filebrowser-config-init.md
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# filebrowser config init
|
||||||
|
|
||||||
|
Initialize a new database
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Initialize a new database to use with File Browser. All of
|
||||||
|
this options can be changed in the future with the command
|
||||||
|
'filebrowser config set'. The user related flags apply
|
||||||
|
to the defaults when creating new users and you don't
|
||||||
|
override the options.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser config init [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--aceEditorTheme string ace editor's syntax highlighting theme for users
|
||||||
|
-a, --address string address to listen on (default "127.0.0.1")
|
||||||
|
--auth.command string command for auth.method=hook
|
||||||
|
--auth.header string HTTP header for auth.method=proxy
|
||||||
|
--auth.logoutPage string url of custom logout page
|
||||||
|
--auth.method string authentication type (default "json")
|
||||||
|
-b, --baseURL string base url
|
||||||
|
--branding.color string set the theme color
|
||||||
|
--branding.disableExternal disable external links such as GitHub links
|
||||||
|
--branding.disableUsedPercentage disable used disk percentage graph
|
||||||
|
--branding.files string path to directory with images and custom styles
|
||||||
|
--branding.name string replace 'File Browser' by this name
|
||||||
|
--branding.theme string set the theme
|
||||||
|
-t, --cert string tls certificate
|
||||||
|
--commands strings a list of the commands a user can execute
|
||||||
|
--createUserDir generate user's home directory automatically
|
||||||
|
--dateFormat use date format (true for absolute time, false for relative)
|
||||||
|
--dirMode string mode bits that new directories are created with (default "0o750")
|
||||||
|
--disableExec disables Command Runner feature (default true)
|
||||||
|
--disableImageResolutionCalc disables image resolution calculation by reading image files
|
||||||
|
--disablePreviewResize disable resize of image previews
|
||||||
|
--disableThumbnails disable image thumbnails
|
||||||
|
--disableTypeDetectionByHeader disables type detection by reading file headers
|
||||||
|
--fileMode string mode bits that new files are created with (default "0o640")
|
||||||
|
--followExternalSymlinks follow symlinks whose target is outside the user scope (unsafe)
|
||||||
|
-h, --help help for init
|
||||||
|
--hideDotfiles hide dotfiles in file listings
|
||||||
|
--hideLoginButton hide login button from public pages
|
||||||
|
-k, --key string tls key
|
||||||
|
--locale string locale for users (default "en")
|
||||||
|
--lockPassword lock password
|
||||||
|
-l, --log string log output (default "stdout")
|
||||||
|
--minimumPasswordLength uint minimum password length for new users (default 12)
|
||||||
|
--perm.admin admin perm for users
|
||||||
|
--perm.create create perm for users (default true)
|
||||||
|
--perm.delete delete perm for users (default true)
|
||||||
|
--perm.download download perm for users (default true)
|
||||||
|
--perm.execute execute perm for users (default true)
|
||||||
|
--perm.modify modify perm for users (default true)
|
||||||
|
--perm.rename rename perm for users (default true)
|
||||||
|
--perm.share share perm for users (default true)
|
||||||
|
-p, --port string port to listen on (default "8080")
|
||||||
|
--recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com")
|
||||||
|
--recaptcha.key string ReCaptcha site key
|
||||||
|
--recaptcha.secret string ReCaptcha secret
|
||||||
|
--redirectAfterCopyMove redirect to destination after copy/move
|
||||||
|
-r, --root string root to prepend to relative paths (default ".")
|
||||||
|
--scope string scope for users (default ".")
|
||||||
|
--shell string shell command to which other commands should be appended
|
||||||
|
-s, --signup allow users to signup
|
||||||
|
--singleClick use single clicks only
|
||||||
|
--socket string socket to listen to (cannot be used with address, port, cert nor key flags)
|
||||||
|
--sorting.asc sorting by ascending order
|
||||||
|
--sorting.by string sorting mode (name, size or modified) (default "name")
|
||||||
|
--tokenExpirationTime string user session timeout (default "2h")
|
||||||
|
--tus.chunkSize uint the tus chunk size (default 10485760)
|
||||||
|
--tus.retryCount uint16 the tus retry count (default 5)
|
||||||
|
--viewMode string view mode for users (default "list")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser config](filebrowser-config.md) - Configuration management utility
|
||||||
|
|
||||||
87
docs/cli/filebrowser-config-set.md
Normal file
87
docs/cli/filebrowser-config-set.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# filebrowser config set
|
||||||
|
|
||||||
|
Updates the configuration
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Updates the configuration. Set the flags for the options
|
||||||
|
you want to change. Other options will remain unchanged.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser config set [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--aceEditorTheme string ace editor's syntax highlighting theme for users
|
||||||
|
-a, --address string address to listen on (default "127.0.0.1")
|
||||||
|
--auth.command string command for auth.method=hook
|
||||||
|
--auth.header string HTTP header for auth.method=proxy
|
||||||
|
--auth.logoutPage string url of custom logout page
|
||||||
|
--auth.method string authentication type (default "json")
|
||||||
|
-b, --baseURL string base url
|
||||||
|
--branding.color string set the theme color
|
||||||
|
--branding.disableExternal disable external links such as GitHub links
|
||||||
|
--branding.disableUsedPercentage disable used disk percentage graph
|
||||||
|
--branding.files string path to directory with images and custom styles
|
||||||
|
--branding.name string replace 'File Browser' by this name
|
||||||
|
--branding.theme string set the theme
|
||||||
|
-t, --cert string tls certificate
|
||||||
|
--commands strings a list of the commands a user can execute
|
||||||
|
--createUserDir generate user's home directory automatically
|
||||||
|
--dateFormat use date format (true for absolute time, false for relative)
|
||||||
|
--dirMode string mode bits that new directories are created with (default "0o750")
|
||||||
|
--disableExec disables Command Runner feature (default true)
|
||||||
|
--disableImageResolutionCalc disables image resolution calculation by reading image files
|
||||||
|
--disablePreviewResize disable resize of image previews
|
||||||
|
--disableThumbnails disable image thumbnails
|
||||||
|
--disableTypeDetectionByHeader disables type detection by reading file headers
|
||||||
|
--fileMode string mode bits that new files are created with (default "0o640")
|
||||||
|
--followExternalSymlinks follow symlinks whose target is outside the user scope (unsafe)
|
||||||
|
-h, --help help for set
|
||||||
|
--hideDotfiles hide dotfiles in file listings
|
||||||
|
--hideLoginButton hide login button from public pages
|
||||||
|
-k, --key string tls key
|
||||||
|
--locale string locale for users (default "en")
|
||||||
|
--lockPassword lock password
|
||||||
|
-l, --log string log output (default "stdout")
|
||||||
|
--minimumPasswordLength uint minimum password length for new users (default 12)
|
||||||
|
--perm.admin admin perm for users
|
||||||
|
--perm.create create perm for users (default true)
|
||||||
|
--perm.delete delete perm for users (default true)
|
||||||
|
--perm.download download perm for users (default true)
|
||||||
|
--perm.execute execute perm for users (default true)
|
||||||
|
--perm.modify modify perm for users (default true)
|
||||||
|
--perm.rename rename perm for users (default true)
|
||||||
|
--perm.share share perm for users (default true)
|
||||||
|
-p, --port string port to listen on (default "8080")
|
||||||
|
--recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com")
|
||||||
|
--recaptcha.key string ReCaptcha site key
|
||||||
|
--recaptcha.secret string ReCaptcha secret
|
||||||
|
--redirectAfterCopyMove redirect to destination after copy/move
|
||||||
|
-r, --root string root to prepend to relative paths (default ".")
|
||||||
|
--scope string scope for users (default ".")
|
||||||
|
--shell string shell command to which other commands should be appended
|
||||||
|
-s, --signup allow users to signup
|
||||||
|
--singleClick use single clicks only
|
||||||
|
--socket string socket to listen to (cannot be used with address, port, cert nor key flags)
|
||||||
|
--sorting.asc sorting by ascending order
|
||||||
|
--sorting.by string sorting mode (name, size or modified) (default "name")
|
||||||
|
--tokenExpirationTime string user session timeout (default "2h")
|
||||||
|
--tus.chunkSize uint the tus chunk size (default 10485760)
|
||||||
|
--tus.retryCount uint16 the tus retry count (default 5)
|
||||||
|
--viewMode string view mode for users (default "list")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser config](filebrowser-config.md) - Configuration management utility
|
||||||
|
|
||||||
30
docs/cli/filebrowser-config.md
Normal file
30
docs/cli/filebrowser-config.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# filebrowser config
|
||||||
|
|
||||||
|
Configuration management utility
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Configuration management utility.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for config
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser](filebrowser.md) - A stylish web-based file browser
|
||||||
|
* [filebrowser config cat](filebrowser-config-cat.md) - Prints the configuration
|
||||||
|
* [filebrowser config export](filebrowser-config-export.md) - Export the configuration to a file
|
||||||
|
* [filebrowser config import](filebrowser-config-import.md) - Import a configuration file
|
||||||
|
* [filebrowser config init](filebrowser-config-init.md) - Initialize a new database
|
||||||
|
* [filebrowser config set](filebrowser-config-set.md) - Updates the configuration
|
||||||
|
|
||||||
29
docs/cli/filebrowser-hash.md
Normal file
29
docs/cli/filebrowser-hash.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# filebrowser hash
|
||||||
|
|
||||||
|
Hashes a password
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Hashes a password using bcrypt algorithm.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser hash <password> [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for hash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser](filebrowser.md) - A stylish web-based file browser
|
||||||
|
|
||||||
33
docs/cli/filebrowser-rules-add.md
Normal file
33
docs/cli/filebrowser-rules-add.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# filebrowser rules add
|
||||||
|
|
||||||
|
Add a global rule or user rule
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Add a global rule or user rule.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser rules add <path|expression> [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-a, --allow indicates this is an allow rule
|
||||||
|
-h, --help help for add
|
||||||
|
-r, --regex indicates this is a regex rule
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
-i, --id uint id of user to which the rules apply
|
||||||
|
-u, --username string username of user to which the rules apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser rules](filebrowser-rules.md) - Rules management utility
|
||||||
|
|
||||||
31
docs/cli/filebrowser-rules-ls.md
Normal file
31
docs/cli/filebrowser-rules-ls.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# filebrowser rules ls
|
||||||
|
|
||||||
|
List global rules or user specific rules
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
List global rules or user specific rules.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser rules ls [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for ls
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
-i, --id uint id of user to which the rules apply
|
||||||
|
-u, --username string username of user to which the rules apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser rules](filebrowser-rules.md) - Rules management utility
|
||||||
|
|
||||||
40
docs/cli/filebrowser-rules-rm.md
Normal file
40
docs/cli/filebrowser-rules-rm.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# filebrowser rules rm
|
||||||
|
|
||||||
|
Remove a global rule or user rule
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Remove a global rule or user rule. The provided index
|
||||||
|
is the same that's printed when you run 'rules ls'. Note
|
||||||
|
that after each removal/addition, the index of the
|
||||||
|
commands change. So be careful when removing them after each
|
||||||
|
other.
|
||||||
|
|
||||||
|
You can also specify an optional parameter (index_end) so
|
||||||
|
you can remove all commands from 'index' to 'index_end',
|
||||||
|
including 'index_end'.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser rules rm <index> [index_end] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for rm
|
||||||
|
--index uint index of rule to remove
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
-i, --id uint id of user to which the rules apply
|
||||||
|
-u, --username string username of user to which the rules apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser rules](filebrowser-rules.md) - Rules management utility
|
||||||
|
|
||||||
34
docs/cli/filebrowser-rules.md
Normal file
34
docs/cli/filebrowser-rules.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# filebrowser rules
|
||||||
|
|
||||||
|
Rules management utility
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
On each subcommand you'll have available at least two flags:
|
||||||
|
"username" and "id". You must either set only one of them
|
||||||
|
or none. If you set one of them, the command will apply to
|
||||||
|
an user, otherwise it will be applied to the global set or
|
||||||
|
rules.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help help for rules
|
||||||
|
-i, --id uint id of user to which the rules apply
|
||||||
|
-u, --username string username of user to which the rules apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser](filebrowser.md) - A stylish web-based file browser
|
||||||
|
* [filebrowser rules add](filebrowser-rules-add.md) - Add a global rule or user rule
|
||||||
|
* [filebrowser rules ls](filebrowser-rules-ls.md) - List global rules or user specific rules
|
||||||
|
* [filebrowser rules rm](filebrowser-rules-rm.md) - Remove a global rule or user rule
|
||||||
|
|
||||||
49
docs/cli/filebrowser-users-add.md
Normal file
49
docs/cli/filebrowser-users-add.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# filebrowser users add
|
||||||
|
|
||||||
|
Create a new user
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
Create a new user and add it to the database.
|
||||||
|
|
||||||
|
```
|
||||||
|
filebrowser users add <username> <password> [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```
|
||||||
|
--aceEditorTheme string ace editor's syntax highlighting theme for users
|
||||||
|
--commands strings a list of the commands a user can execute
|
||||||
|
--dateFormat use date format (true for absolute time, false for relative)
|
||||||
|
-h, --help help for add
|
||||||
|
--hideDotfiles hide dotfiles in file listings
|
||||||
|
--locale string locale for users (default "en")
|
||||||
|
--lockPassword lock password
|
||||||
|
--perm.admin admin perm for users
|
||||||
|
--perm.create create perm for users (default true)
|
||||||
|
--perm.delete delete perm for users (default true)
|
||||||
|
--perm.download download perm for users (default true)
|
||||||
|
--perm.execute execute perm for users (default true)
|
||||||
|
--perm.modify modify perm for users (default true)
|
||||||
|
--perm.rename rename perm for users (default true)
|
||||||
|
--perm.share share perm for users (default true)
|
||||||
|
--redirectAfterCopyMove redirect to destination after copy/move
|
||||||
|
--scope string scope for users (default ".")
|
||||||
|
--singleClick use single clicks only
|
||||||
|
--sorting.asc sorting by ascending order
|
||||||
|
--sorting.by string sorting mode (name, size or modified) (default "name")
|
||||||
|
--viewMode string view mode for users (default "list")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options inherited from parent commands
|
||||||
|
|
||||||
|
```
|
||||||
|
-c, --config string config file path
|
||||||
|
-d, --database string database path (default "./filebrowser.db")
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
* [filebrowser users](filebrowser-users.md) - Users management utility
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue