Compare commits

..

87 Commits

Author SHA1 Message Date
Henrique Dias
20d80bb054 chore: version v2.0.3
Former-commit-id: 946abfab3889ee4df0dda76220b13cc0ebc4d3c9 [formerly 34dff77bc4f5b12eb8e47e379d21caa0e55dd947] [formerly a3b1b9741b197f1fed9b53688c61d1fd474cdd43 [formerly eeb6f3207e]]
Former-commit-id: d6191757d2495b28af08ed8e7de6dd46c90e7eb3 [formerly a49750caf93d703eb77df3d7fba2c0c234249fd5]
Former-commit-id: 98ac0d3e5e19a270f0c57dd5e59b3ee22fc05af5
2019-02-15 13:05:33 +00:00
Henrique Dias
cbdf3cafb6 fix: possible fix for proxy auth requiring login page
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: aa32ec7e27f24aee497d3c1a6bb3cda4f3f30265 [formerly 92f1d95d44e0a1c0b8dd6d81b467829c8832acc8] [formerly d8c05dc476ae88b730547cce1e3644c69ee17278 [formerly 84f108f1c5]]
Former-commit-id: 9f766a5b8ef847569fb8f4f16540c256fa9ca92f [formerly 0b6b1e48435b3fe5819327e311f08eb179a19b3e]
Former-commit-id: 046dfa8350d2f1b4e7fb789fb026069e137437ef
2019-02-15 12:58:45 +00:00
Henrique Dias
1259fc1bbc feat: remove version cmd (#675)
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>
Former-commit-id: 04a60c58f20d63ca7b25731c41e144bcf0f538cc [formerly e6e179799b64779515051df53352df5e63edc259] [formerly 0689eba81ff5f7ee3ea75db37b91cef4d6d8f35c [formerly 85899acae6]]
Former-commit-id: 96ca0cadb94131ddd3b57f0f11ad629edf687e40 [formerly 50130c75d39e67b15a645e7f4879acf34a5d6620]
Former-commit-id: 53b8120673a82217c3625de161d4ec57a96e1470
2019-02-15 12:54:44 +01:00
kolaente
4cf1f2f6b4 update dependencies (#668)
Former-commit-id: 8987aa3ca60368e28dbb54e7830f76ae9d8863da [formerly 3fad0ef36350b59b3a9ab613ae51df58c57abd49] [formerly 062602db41695fee69a8e7626442741fcd5d592a [formerly 4a4f635816]]
Former-commit-id: 9f624413cae53841cd306672b3cc8d6bc8c9a5ef [formerly c5df323016df89b2eb1ded85ba80ac8e2af0f52c]
Former-commit-id: 15a16269ed81b00917b5097e203cda55de8fa120
2019-02-08 14:37:14 +01:00
1138-4EB
a90bb28cae Create directory of database if it does not exist (#650)
* fix: create directory of database if it does not exist

* fix code quality issue


Former-commit-id: 88c95717436489ff9014d88374775cc4c3f265b3 [formerly dc56f2c1c3bf46aac9a23e700780743ad15866c8] [formerly 92c2f9a68d575fb6e13453df6690c72f05ca9598 [formerly de205177f2]]
Former-commit-id: d0888c2f73fa691b900ddc7cbece6dbef985da96 [formerly a7495f66547f1814d31b1ca41148133d23198fd5]
Former-commit-id: a60afcd1fc6aa5cc88559025d7b3176b60117544
2019-02-03 15:51:17 +01:00
Alexei Yuzhakov
e86dfbe8ff fix: case dependency and sorting order (#661) (#662)
Former-commit-id: a772b30907a92818e5c109adc0d382af7ddf40dc [formerly 589c237e7f1230f16970c0b68263d4174dbe98f6] [formerly c3a0e4964d028dc06e5076869dd92975eb3c4699 [formerly 17b514510b]]
Former-commit-id: 8e2c49d8fc18df68bb3172fbb98a97c98a5eb818 [formerly bc396cbf040a5016a26cd5387b14e9f55f8b74c6]
Former-commit-id: bda1997f4292c213e907ed0d8e566c5ab931edb9
2019-02-03 15:50:08 +01:00
Henrique Dias
437a238aca chore: setting untracked version [ci skip]
Former-commit-id: 43d2b1b4fda66a7ae412a5091e93d35c8befc341 [formerly 32d7d602dc1e4a9696fb7fb8230493cccbbece0d] [formerly 7f4524b9c180a1ee538b630902724252203dd439 [formerly 4a2573830e]]
Former-commit-id: 0927241d911bcbdc8d8dec216fa019e5243f9805 [formerly 483ca4ba1feb2de599615d6e738bd3568fd5e783]
Former-commit-id: b16c5e4a58438aafc5e64e30bb9fb031253c8051
2019-01-29 09:10:21 +00:00
Henrique Dias
e5fa0772c6 chore: version v2.0.2
Former-commit-id: d9e5bcaa8ba7c947ca2910e144456286b2e7dfb2 [formerly b35a7f02953dfa884ecb59b45cc7a9173c1dc247] [formerly b207ecb8ac3d2ddd9ebc12cf12ec0cf3badee056 [formerly b2c70377d1]]
Former-commit-id: 134324213f643fe4637eade2348139a78aaaafbb [formerly a3d41a8bbceacb5f2719353d969a1e3b1ef7118a]
Former-commit-id: 64796269842b1c5860b0f22405506bfca4b2e7e0
2019-01-29 09:10:06 +00:00
Henrique Dias
ba5c67d9da fix: external auth by using a different auth header (#649)
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: e7c2574e40390d402443b23dd7aee5e5b18e5131 [formerly 20463dd5229b0e1f572a883e83db9e43bf8c8ff6] [formerly d2910cf8b18b20b13a46ffad23183c89325ef06b [formerly b5e61bc979]]
Former-commit-id: d26442e70963119ed3e6abd1ae894294eb12f1ed [formerly 9b49bbd9a959595ef0c26eeadd1fcac96e2b338c]
Former-commit-id: 1c8b2261607cdb88a3c685311e165e517234e498
2019-01-29 09:06:30 +00:00
1138-4EB
f3d007025e remove addon docker-ce from travis
Former-commit-id: 169fb5f6a61249e5420bd8cbf758b961847f86f5 [formerly 05042727df836dfa48a7099f8c6b18bf4052ea52] [formerly b1f252a7bfbeb01e97e2b930bd9b65eca72bed63 [formerly 14a3b5a417]]
Former-commit-id: 2642889d6f7d445e2e63c7f4cf09581598bb6ff6 [formerly 4fcb552e7678e609b0f706e9e31f5fc2e49d5330]
Former-commit-id: 014ce24c903c0eb9e6dabe3fa89cf2c609b2d5aa
2019-01-28 03:35:05 +01:00
Henrique Dias
e7a39808dd chore: setting untracked version [ci skip]
Former-commit-id: 9ef650ed689aac71ec52c4613c7a7b35721604ad [formerly c539dee8f5474a48c9b5291b8ab1aede6896ebe1] [formerly 9db5ced0957f01e287205c2001c47c03b6543f44 [formerly fa4b100c85]]
Former-commit-id: 3b9324f933906b3d898373f418ca462c559f35ab [formerly 6b5d61a31cef4a308139383841c6ef0fa2f2d151]
Former-commit-id: 6e6ec1ef9caad0fb0ad722aaa866647ebc78d107
2019-01-26 12:11:29 +00:00
Henrique Dias
2b7aa7a0c9 chore: version v2.0.1
Former-commit-id: 1ee923d5e301cbc091885dc523baf9f771a5fe26 [formerly a7fc98143b3fc481d04e64c9a9608957475bbece] [formerly 87cca4477009a498c17c5779ec33964513139325 [formerly 5b097eec5f]]
Former-commit-id: 28bf2572b9e3e8258d57de8c0905a8aaafa707c7 [formerly 5de3ab416d621dbd54cd58cf0bf34273017144d9]
Former-commit-id: 603e4589dd55068be106f240a57ec1ee04f8760d
2019-01-26 12:10:55 +00:00
Henrique Dias
4d8f0c532b feat: update front-end
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 2153ac8242a70a057f3573ee6d5afca1824a61bf [formerly c3a4cd6efdfcf3d7d568548763d9eacc1f2a1679] [formerly 6ae07ad768953558d19885c5e4a5b97194ae18ef [formerly a4fee75dd4]]
Former-commit-id: 09c70625d4c30701a49a063835e22a5b74152ffb [formerly 2ac34724d3074d99a21878b5048d9886ce9bf04d]
Former-commit-id: 6039d9b1533d84cc930db13f16a1acb6222610a5
2019-01-26 11:38:49 +00:00
Henrique Dias
367e251a0e fixes: correct users importing id (#645)
@princemaple could you try out this branch to see if it works on your end?

Former-commit-id: 12c782316c411fa3438f167c12634adffb5e1adf [formerly c613ff6624c55292f532d7772e866cf6c81241f5] [formerly 11f03e54dae28fa773227d85259cfd73c5750137 [formerly 9c42269b62]]
Former-commit-id: 04bcf5803397877d00ba48ea6d0b00246dc07b3d [formerly 4c1bb1e95375777aede2c0911bdfa9ce46273b37]
Former-commit-id: 601db641f7d7b01c14903d0cc27085e3d7080c3d
2019-01-26 11:37:33 +00:00
Henrique Dias
cc6f2f8bec fix: encode URIs correctly
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: bf213b683c7cf6a42bd5a710e08ea2ed1cbe6c8c [formerly 7033f8885f037a8fd17aaccc7c95741cdd8a7f15] [formerly 1156eac936b97cc1821f0ffc28f09c977b434945 [formerly dadfffbd31]]
Former-commit-id: 5afbd473b14481514e137a191aa75c3860efbe68 [formerly 691b7b346559672fe41fd2bc7430256ab8512b2d]
Former-commit-id: 0aec5af50f77e3f4f6701b3a4e34694514c64cd5
2019-01-26 10:47:34 +00:00
Henrique Dias
007fde8186 fix: linting
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 3b1344ed7b9b7c41f7188a3e740cf69f3de7ebb7 [formerly db8acf75870cd31f5077629b2f8bcc6606afb606] [formerly e2a38027b43f835c13933188d0c4e14119f78661 [formerly b90d39dfda]]
Former-commit-id: c521ed975cc868b106344c4212707aa1b1eae19d [formerly a3f0dace392521435350c78e40adfc9ac6c576f2]
Former-commit-id: 46c88075964cdd102f5fd2c12e1b09fd94eba6b1
2019-01-23 15:58:06 +00:00
Henrique Dias
552f8168ea chore: update frontend
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: ee0540ac596192045f940e0bc4f47d29b6c2351f [formerly d90bc812c6a77c9838a711fb4e00d1d62a6bfa04] [formerly 90128f33ffa10f584fcab30a8a1165c06b6659e3 [formerly 2ba176a319]]
Former-commit-id: b677fba611975bdbeb56fd541ac7bedee39e000e [formerly 5ad6e6b025d20d171d4a097cc5895b4f7f38b9aa]
Former-commit-id: 91d3aea37524297ece1d797df6bbf716cda77553
2019-01-23 14:49:18 +00:00
Henrique Dias
42426e3e69 chore: setting untracked version [ci skip]
Former-commit-id: 28403ab07de56efe726c60b5b9394652ade286c8 [formerly f98ae829dafc277f9af53195a359ddf4177b2adc] [formerly 9bee729afab603d71e0f386f2dcf1d68bf117be0 [formerly 846127d164]]
Former-commit-id: 03a5583ca594f0cce67b025d913f68ad5582285f [formerly 8ee04063a261f3f85b9e19b86c9a96e2bf6e78e6]
Former-commit-id: 683769c4b55b6dbddf47e66e1e6760e9f961f508
2019-01-14 09:04:16 +00:00
Henrique Dias
e706391934 chore: version v2.0.0
Former-commit-id: 1a6e6ec0cda359306ef81deb4393e6e588005674 [formerly 45bb5b1b929da50fec506f27a50e6ba492374b60] [formerly 41a597953b6d48ff01ed5a28817e9c9436db300b [formerly 145b391315]]
Former-commit-id: db2d7837facbec0ab84275550934d70956d28add [formerly 0d47b66dfb9c802f0ee885b43a034f6cea044878]
Former-commit-id: b5ddd38e9879458640ec3e9449c72e0fad4e4df1
2019-01-14 09:03:39 +00:00
Henrique Dias
58ff28f84d feat: export generate key
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 3088476c44d4608e2b8c90d3c274b6db17ef30c0 [formerly e40d3ba193e0b54bccce8c13459032f51af84c7b] [formerly 33cabf26abee5ef4f1dd63e423fe1b0fa7ae4f7c [formerly a3daac84a2]]
Former-commit-id: fe022f36fc4d7d8442df8c0fa4a86e2ff3ec328e [formerly 1d949d0ae88b3b5a9b8e4da5c3b6ca814a319589]
Former-commit-id: 36207ddeb7eba335f5fe73ea9dcdf6bffc6265bf
2019-01-11 20:25:39 +00:00
Henrique Dias
74a1ecbb89 chore: setting untracked version [ci skip]
Former-commit-id: f8b61a3532b72c804933bf4ba63a111b3c8a2955 [formerly d076a6110407d517bde07846bf31f6cae710e0c0] [formerly 435d24276a43b69dec81ff9666d78f208116a577 [formerly 6dbf801370]]
Former-commit-id: 7ebd072dccb015521deb296f0151c06f17b5dcf8 [formerly 9f8c86143171142fb3c108ba32c7eab63a5632db]
Former-commit-id: d28fa2428da947bae7029257d04497d581c18d99
2019-01-09 21:52:14 +00:00
Henrique Dias
5fa1711a30 chore: version v2.0.0-rc.2
Former-commit-id: 39889c9895dba58578db931c4c55a1c25b822672 [formerly ebdcd02361e6499301262051376278a9dccecce9] [formerly 4ff653acbd767dd37a2b86be607bbd1af0fb7cd7 [formerly 0d0d9ef5f9]]
Former-commit-id: 0ce6e7f7871bde28b24e1b596af786f3e8dbddf9 [formerly 7d3a69c30e1af1e5b028bbcb926a28341a3f71b9]
Former-commit-id: 81cf5a20fa5ae3077337d968f686df58923b5b5c
2019-01-09 21:51:39 +00:00
Henrique Dias
4bc6a23143 fix: no db error when db size is 0 (#629)
Fixes the error where File Browser would fail when the DB existed with size 0. This closes #628.

@1138-4EB I decided not to check the version for now since it's the first time we're adding that info. Only the next time we change the DB structure we'll use that value as reference to know how to upgrade.

I did not check it because the users running rc1 would have some issues with it.

Former-commit-id: e8b07ab919e9c24feee8f6f2dd6df30df1e2325f [formerly 3396f05b4a1a07e1b993b7c07d1afd2def9cabbb] [formerly b43eb8e400603f99baf7966a58156dec7bd47420 [formerly c1c57c6525]]
Former-commit-id: 1115d0cf8a601feafac2b33cb46b813343f4fa8c [formerly 05de6caf8cf747a95fbb10587f0a767dfb8b8774]
Former-commit-id: 840c1a278cd45d36ce8538b7c5fe97923be05ca3
2019-01-09 21:37:47 +00:00
Henrique Dias
b578e2196a fix: search (#625)
Former-commit-id: d9c9234e87c190d847572c1fe715cf745ba44b53 [formerly f8b5f600fe39be8b47aece870ff24756c5dcfc6d] [formerly ad1cbfd739888d4cc3a1db882a98251cac3b89cc [formerly bafe9f0ad7]]
Former-commit-id: 493e4f3e4b42092201cfd1f99d8ee6cd3eb25dfc [formerly 520a9df6582492f24906f8f3f5c5e90cf5a02acd]
Former-commit-id: 17484d0fe391a82d5ee0dc990f4b1f039d305f4c
2019-01-09 15:18:03 +01:00
Henrique Dias
866375141f fix: strip baseurl (#626)
Former-commit-id: 5daa76063fbd8766115a722336d544d2f6156c74 [formerly 3d644f06b38b6c20f5549b3f2f780556881f216e] [formerly beb819187116079ce83329e85bb1b1d5e6b908ea [formerly 73e0acaa71]]
Former-commit-id: 01dfc4bb6016d6c3de2bb932695e7ca6b9185ff6 [formerly cdee480885c5050d9311f41f39ef7b4feefcc602]
Former-commit-id: 82bbfed106ddae78f22d01cabc11f314c19a1796
2019-01-09 08:13:57 +00:00
Henrique Dias
a3b9807717 fix: clean server struct once it gets to the handler
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: a9d9c2cd9a5fffa71409ebb0867faa3a0aca6e9f [formerly c3359cf3ff2633945a9c8dd5ea8eff5a77484364] [formerly 8921b27f322e426cf2f2f07dc682e1fffca162e0 [formerly f1c86054b3]]
Former-commit-id: 078633f5c5b1aaeafd6b078b5e006c5e05f19c84 [formerly 9013496a12cda3bef58215a3f5df4eeeaa677eb1]
Former-commit-id: 420076748685f1b8a23053c5fe159bc5e86942d3
2019-01-09 08:13:24 +00:00
1138-4EB
d47774c828 fix: caddy sed package name
Former-commit-id: 23faa66a6010168f64545ead1beb5f41db3ffd76 [formerly 335df9ff6c4799677119e72f6bbf5a00e6709c43] [formerly 54ebf5831102f306edb07b40cb518262787a9305 [formerly 5a9f0a0d81]]
Former-commit-id: 5203ad4f906f8f85c7ece2d7beb14eedfa907cf7 [formerly b58ea071061a8a045fbbb9301b028774f9991696]
Former-commit-id: 7ae9ac04e135c449355a8da8845e0951fde6fe92
2019-01-09 03:13:54 +01:00
1138-4EB
bc4ac5eb9b chore: setting untracked version [ci skip]
Former-commit-id: 55909b40916ed0b65a2d588cac5aa171b43c4482 [formerly 52607bd0e51b455281480ac0d8c7fdc5a4a663af] [formerly 75ce16e7e290c9fba9f4288507a4961a4bb0b10b [formerly a55e0d24f8]]
Former-commit-id: 15936e40500010751e70401bd6f0574f614d9ada [formerly a39e7b2a11d0379964f5e911447289910406e0e5]
Former-commit-id: a87b512c8f08cdf5affff568ee96507575e8e2db
2019-01-09 01:53:36 +01:00
1138-4EB
f16ec4b45c chore: version v2.0.0-rc.1
Former-commit-id: 15b5351860f405fa69e33eac6b1fab2c5cdca545 [formerly 3784bd141b58be668dea5b55daf1581a8cd368ff] [formerly b35c153dcd6eb02bb9a989cfa94524609c69b2dd [formerly c1f344e9eb]]
Former-commit-id: dcb76cb145e62812a451e871385be3e7065c09f8 [formerly baad8aadbacf6ea9d17e70ecdff9272e341006c2]
Former-commit-id: d58d71dea43f6b2da1b5600b5d15a15d82fa9f1e
2019-01-09 01:53:36 +01:00
1138-4EB
f8da8bc985 fix: wizard pushRicebox non-existing envvar
Former-commit-id: 4a6d00004f968e76543e3d20ce493a0ff5e4cd1b [formerly e36e6696fd79defbbff9683c96a53920c158aeef] [formerly 5d1798ef2f3a97a4d5e772500acfcb8d47a6a75a [formerly 9f6e51b251]]
Former-commit-id: 1a74df1209e3216eb5cfd780406b75136a441d36 [formerly f72d4c934ecaa9c6930cfa9da7d1974757759af7]
Former-commit-id: e0e020f7937a943976c78c0d212a1076ac2c8ee4
2019-01-09 01:53:35 +01:00
1138-4EB
0ec589ca74 fix: update to goreleaser dockers changes
Former-commit-id: 4c10c5e07f551de30964ae18cfdf4e868efff84c [formerly cc244cf72188258270d20d30781170603513cdd7] [formerly a28388f0d471fae4d8791c1e31b7f9d8b4299f4c [formerly 27d1a95310]]
Former-commit-id: 854366de95a9065503b779581c4d0a4c25b37b65 [formerly 72860fe1d2b1dfcbfd2e2c0797e55a1772b81a38]
Former-commit-id: 7c9b7d2b86fe881a7b71f7619b698cce5c3f98ef
2019-01-09 01:30:56 +01:00
1138-4EB
4e4ac88cbe fix: goreleaser main file
Former-commit-id: d4f2b341e949bfce045124115123b2748ab4b438 [formerly a92a7d72c9077d77be55f9d76b94a104f0be992f] [formerly 3ee3bbc16944230a4c5f7b5b92c19131f08bc5e7 [formerly 8c2c298d9d]]
Former-commit-id: 68b9c5ecebf51002b823d4ef0aee9746a6f22e76 [formerly 441e5f99c76a2d9594f3ee8c12a7b889e6edeee1]
Former-commit-id: aab01c50e0b68dee6472455bf2c14d254fee57f7
2019-01-09 00:37:59 +01:00
1138-4EB
0762516a18 fix: wizard -r requires an argument
Former-commit-id: 0f180b2a9e78ca551eba70705f812e349943d468 [formerly 20bf6d4bd2ec6165c02560725b698278e17e24c3] [formerly a60dd0705c3d59d9f8214fa4bb38b39259b2fd5d [formerly 43e494b0f7]]
Former-commit-id: 6c7a31c95851d3759cef822976bcb41ccf45cbbc [formerly 53b885258e59bb8366290b30b2aded5ab1e361a6]
Former-commit-id: 8311ae09ce36822171d55e3f855bf66a9a2b3461
2019-01-08 23:54:37 +01:00
1138-4EB
41272e67a3 fix: wizard release
Former-commit-id: ca21ec5987a64aea7d155d5ce532b452d502d1e8 [formerly 0f4ae7305653e52c36bd2e05b6328e4b6a21cfa7] [formerly 8461ae3cc270e84de3ab0a402d50484b1a417261 [formerly f218c2a791]]
Former-commit-id: 1e7aca3d5837c39a5073bd3d65847fe566929ad0 [formerly d210374bc803529ce4edf7a6d83bd3c4ae3f1bc5]
Former-commit-id: 0eaf26b83f446f6022da9af1be334f9eb452b5a1
2019-01-08 21:22:03 +01:00
1138-4EB
da7d1db06c style: rename functions in root.go (#623)
* replace isFlagSet with Changed

* style: rename mustGetStringViperFlag and getStringViperFlag, use getParamB to read noauth

* style

* style: move

* fix build error

* rename getServerWithViper to getRunParams


Former-commit-id: 2d6d9535247e9de01ca0741726665f7dfef1b1e6 [formerly 8fa7b76b92545e0b91bd06fd7b21247e921bbb2a] [formerly 3b7eefee2ac5120796c2a13583ea0b0b2d1ccb89 [formerly 2c526077c0]]
Former-commit-id: 57912c62749c5890285657c1b4b637925d480ccd [formerly 03b4cd9551d77589f25881cbf693c6bf8390db4e]
Former-commit-id: 77b9083e39607aedcba9ef9b4af9a94535661ffd
2019-01-08 21:10:27 +01:00
Henrique Dias
cc428d3cd6 feat: add no auth to quick setup (#621)
Former-commit-id: 56d5c323e0f176247779946e04664990b13146e8 [formerly 43ee26a4e1929560135c81ef2d1bfb60f6d15815] [formerly db983bfd8d1e9cea1e749b02ac11d933af05624b [formerly e0a3ce95f2]]
Former-commit-id: 20d14a1193271ef23c090dab1b3828ba7655b687 [formerly bdeb651589e0ca20ff669bfeb85ae0358e8e1d95]
Former-commit-id: 1f00d921e2a7e60f3a290b11603acc6a67a874a2
2019-01-08 18:43:04 +00:00
1138-4EB
9a54abfe62 chore: style and gofmt (#622)
Former-commit-id: 669963fd1d51d7e2d32520ffb0c1ecd5e4224c78 [formerly 8ef320b07699c842ae0325e4665d570fa4399481] [formerly d171df23332dff1d33f00a7407f5dc3f6dde1ef7 [formerly d0fd97d943]]
Former-commit-id: dc9a921b032e1acee0845186e7a009badbe768c5 [formerly 0c8a0f739bd1d984ca0db2b0623da5fdb813f4e0]
Former-commit-id: 3005cd15261952387fde1adf13770e2c1ee52749
2019-01-08 19:40:14 +01:00
Henrique Dias
c50a9b29bd Merge pull request #620 from filebrowser/chore-docker
Former-commit-id: 50fc6f67b7d60b6d30368e58f7ea612af0eefd33 [formerly 01f966c6f8f313a0cfabe43ab547724cb550dbd8] [formerly fd099dd6be88c96aece03fdbb83d716ac23eff16 [formerly a70b9e20a7]]
Former-commit-id: 24c6dfa4f95567a8c6a845243de4649dbafce660 [formerly a07b3b77be33f64ad9deb616c032e31b0c6ea2b2]
Former-commit-id: ad7ca28cac44bbd54e0f622aee02ef87c0777cbe
2019-01-08 17:38:07 +00:00
1138-4EB
3396218ff0 chore: update Dockerfile
Former-commit-id: 2ce789dc1facaee4afb7edcdd922f304b0b2cd69 [formerly 52c535b516b35996190d4bceded1502b622bed10] [formerly edc3b73e09591edf8bb352f047fe64da39c58f4e [formerly 5a0547b467]]
Former-commit-id: 87c2a4a3e4002a0a676470f9fda207486673b525 [formerly 0d790b609d7b0d96dc273f2241dedb0695b8d351]
Former-commit-id: 56b586a9cf0f851ad69b1db9e8e7046b5d57523f
2019-01-08 18:17:52 +01:00
Henrique Dias
04392a7328 feat: quick docs update
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 24d7ca4ba681ae4395cb23620fb92c19fad79279 [formerly f22f42daaaa18cea22f1a5c15544568d3c7bbb2a] [formerly 04e6ddffc5371fba2dfa1c0322326ed9b28ee73a [formerly 66010d877e]]
Former-commit-id: 10e7edbfb312764a8ba335a10aab2ca26392db47 [formerly d2fa614d1938fb18644957728634808b1d4acf5d]
Former-commit-id: 65727295e4f0e3758d235e08dd62cbf737b5904a
2019-01-08 17:00:36 +00:00
Henrique Dias
e1026a1fb4 feat: add docs info
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 4cc444f7168a76920c2821534027f121b7b0b0d5 [formerly fc1b399bbcf6cbd0a26db133ce644192664e4987] [formerly c71a14856c55345626eb7982e2bbe9ca0b7a1aec [formerly e68af011d4]]
Former-commit-id: b6ec8dd8817ec6bf028e1e7df299f8dd720628d9 [formerly 99077c4ca6faac93a1c956d96833faa3f2c40fe6]
Former-commit-id: 620919fdfd9f213cc61e4ce3b4e8650096a489b4
2019-01-08 16:37:02 +00:00
Henrique Dias
4703e0d36f docs: improve some cli docs
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: f7710895557fc2f181dc04b07cef1bf8e612bd9b [formerly bf7a392bc9e83971ddc9ae673f4fedf8772d3ed1] [formerly 06404f0eb55f410bb6cab197021bdd5e5c0991a2 [formerly 2ba5a4275a]]
Former-commit-id: 5a676627143edb8d7ca474b564fe2ab3c6b610c3 [formerly edbc0e60c66ab62df1611cda766fc8b3c8ac837e]
Former-commit-id: 2925d9dd6471f900c59adb495a5c2f7abfe0026e
2019-01-08 16:09:25 +00:00
Henrique Dias
2e1553542b Merge pull request #618 from filebrowser/chores-version
Former-commit-id: e4a254c445bb6f9e6012800101812847f2f7a165 [formerly 39f8661c8cb8a37dd379bc87685fa266e2c98cc9] [formerly ad43c963e3adce98b64edb3324f9c4912b4a5623 [formerly 58edf878ab]]
Former-commit-id: 66301024e8d6c34070382fb0b9db8e25a1055da1 [formerly cb29ecd9eec4ae6ae0cb422457ee1d794a51df20]
Former-commit-id: d4eff084d7aad39e384ec9cc0cd3227250a195d3
2019-01-08 15:55:06 +00:00
1138-4EB
438371fc9a use log.Fatal instead of panic
Former-commit-id: 5f02ba76b35ff67d57114729fc08e58fadc11067 [formerly afdbc0fb9d46042f3bd02d623d7d762a30edbd3b] [formerly 13ff37a836606b90cff13e0836e679983eaaadec [formerly 7a775c42a1]]
Former-commit-id: 99e8cbc7708b4c6c197220e21eced17aa11e242d [formerly 1404d4021cbcce848c106a8b15ee9bd989afebf1]
Former-commit-id: ae93bddd201e69d8069cc1ff8989e75d1adecec3
2019-01-08 16:21:03 +01:00
1138-4EB
6712fa580b enable version subcmd and --version flag for root cmd and all subcmds
Former-commit-id: a9e681d47d47f9f09a22d8479705f62672f509b7 [formerly b7d17db57156675b06897bc289fe616628a47abb] [formerly e06d3b13b547356fb928288b09aaaaf1a60b8950 [formerly b4708348c6]]
Former-commit-id: 43b18221e14269bada106e867b666f67a692c992 [formerly bee15f87fc2ecf5e8b4a0d74f41bde7617aef12a]
Former-commit-id: 733aaa6f3d5e3fc80dbb65ccc028d9f6ff6f73b7
2019-01-08 16:20:23 +01:00
1138-4EB
b660404bfe reduce code duplication
Former-commit-id: fa4946eff4f979e8fd3550a0b11f3ab147177b8f [formerly cd071c220157452a095325c049d801e99d2f8a94] [formerly 7a619be88fb162ddbf897387a31577fd3e94bfec [formerly ef56112711]]
Former-commit-id: 692897eac98f1d8d4a916d0ef0aa342b994a186b [formerly 69d9f127de2deadbbcaa0174fc1a4613ce2cb93d]
Former-commit-id: 49c5aef77aa74ec8352368627c98b9e50d417228
2019-01-08 16:19:17 +01:00
Henrique Dias
6ab8779948 fix: root
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: fc5ff2d807b0b8c6857c71311f061c7322af32df [formerly 19bd77dbaf743c53e7c8db08930e2a9ce6c0b675] [formerly 89f54b49d9d705b9a016827ceb1e7c0face0e56b [formerly 88644664e4]]
Former-commit-id: 3ff15179a86d330ff5f2a7f6b49b890d3ecd34fd [formerly 483a8a99b3b9dbe13ded1cf7fd608bf27b35a4d0]
Former-commit-id: 8115bd29a2356de1dd1cc2950248c4a3c07580e4
2019-01-08 14:43:46 +00:00
Henrique Dias
dd16a2e836 feat: update frontend
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 5b039e136b6100689309ded011501fb4a6dcce76 [formerly 54ace8e99e7ad81101cebb0dc44c5ad67b4af3b7] [formerly 1756700eaf0dcffd6b3888e613925905a123390c [formerly 68bdf65595]]
Former-commit-id: ef1cd08c5a36d4f28a6fd6d7d906b36f7b377051 [formerly d14c62b4510730c62bb2fba982c4cbaf96c804ec]
Former-commit-id: ed5c968279e630468c2ba6d1823a86153be78d4c
2019-01-08 14:29:18 +00:00
Henrique Dias
870b5b4079 fix: use server options from DB too (#616)
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: bb60ec81f56f5d761ef76b2c1e26bbe83210f434 [formerly b28f83c65e0934e138a1710549f52e0f66a158e0] [formerly 7138a3215ceb144afa0ea45be2315f30b052109f [formerly 72069207c6]]
Former-commit-id: 0fb7ae6353f786edd93a9166141bfa446dd3dbf9 [formerly 5cb07e8c96a7cfca1aec5843123e1abc4ca8578a]
Former-commit-id: 914c0348516f072a9ccce4e105327feaaddb4cc0
2019-01-08 14:07:55 +00:00
Henrique Dias
66d485f639 feat: add replace opt
Former-commit-id: 7cc788c0560fb54cd098f4b6039b42d46fd78561 [formerly 7fd126f9152827162e9cf17dcd3ea293793ce504] [formerly bef00ad6a024835145bea635511be47b7e1f8a16 [formerly 76bf72902d]]
Former-commit-id: afc342bb45e4b8d3f059c33f2c60043f9612210b [formerly 149f17999b12be474cc91e2b821a528ce98adfcf]
Former-commit-id: b855a6691f1661b5d8af13a58f80773eda2d8a96
2019-01-08 12:03:34 +00:00
Henrique Dias
684366e050 feat: simplify flag adding
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 520945c4cd979999ce7da8cc60544e1e390de3fe [formerly 5c73f94c28f67f0fb25ecff7ce999480ff0d873f] [formerly bdb85560ceeac89408c8588213ef88c5b774c8c7 [formerly fc06f642f2]]
Former-commit-id: dc0c81b78cdec8656bd25dad84f9589486300d34 [formerly b18cd1e0bced8b34b31863fb7c4e714905bbf0d9]
Former-commit-id: cd84e54f28b9c56c3ced9da79f7fcb9a659f674a
2019-01-08 10:50:37 +00:00
Henrique Dias
01929c72ea feat: make server options a struct (#615)
Former-commit-id: a54bc7c8a0fb700d4f90f78c54d6cb028e8acea7 [formerly a0946a1d5b82ce5681684bc0f871bd892c4d6c3f] [formerly 6b23b0abbe5cde6a6348f982975d15a1161359de [formerly 0e7abaa7fb]]
Former-commit-id: cf9ea1110bcb8b5eb30ecd25db1928659ecea922 [formerly 38cebeff1f9e6b03760f7b277a3941f7bd733fb5]
Former-commit-id: db44c1d5ff8fed361849b5bf1fe48e04c75d7ce7
2019-01-08 10:29:09 +00:00
Henrique Dias
802318f903 feat: config/users import/export (#613)
Supports json and yaml.

Former-commit-id: d36b07953ede1842942b7ab477effeb2e5aa7d5b [formerly 51d0d5691d19e0649935816779a34b1b700e088a] [formerly 342f636293be8e38e7907453a67c67e5e9195c78 [formerly 73b8d2ee7e]]
Former-commit-id: 8ef6a1563ebe425a15a8229165d2ddb043cefb21 [formerly 01c4ac1d89e0d5c6ed16bb7f23c2bbe62085d6e5]
Former-commit-id: 6d197ee1931889571c61ad0920e4352d4b02b264
2019-01-08 08:57:24 +00:00
Henrique Dias
b9acd275a2 feat: remove useless code
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 7cc4780fb876e5cbb50a2918bb7f59ef27c9df69 [formerly 172f465b8292a1427ca957034c25bd989b27665a] [formerly aa4115896142f42f126f3cce34256937b03c5e04 [formerly 46b221533a]]
Former-commit-id: dbe998a5acfe50a763cef63316c824c49b626dad [formerly 92d7264061e532ceae3a98861f434734f4488b90]
Former-commit-id: c09fb725461ad375bac46a2648a588a1b6d88ffd
2019-01-07 23:11:00 +00:00
Henrique Dias
1809a3638b chore: update ricebox path on caddy repo [ci skip]
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 92566ce4d867c9ca04840bc8a10c72620bef7988 [formerly b417064bd37ebf0d30bc38b6f2e463a1ca3d5531] [formerly 8c167e6c20cce29f068d9ef574e5a52830c4b2d8 [formerly cae8809da0]]
Former-commit-id: 45bc3ca821d054bb8b96bbc58f8206268cd944b1 [formerly ce0f48e3b4673ae70aca38fe427a334c8129b4ae]
Former-commit-id: 69b5cd589a27d444adbb83b8f1ae7fbd7ed4709f
2019-01-07 20:59:55 +00:00
Henrique Dias
5a83d6736b fix: rename global scope as root and fix root md
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 856c18bc9cf98a27b6cbea923b231e0aaf279190 [formerly 201c1a0294947930a7d0706af72ce719a8cc3b98] [formerly 0253e57e2994023e798f6fb0ae76f9c21d18fd69 [formerly 33a58c999a]]
Former-commit-id: e7d88b22207125c29ea85a5a539653a54584999c [formerly 77cb1e0172cdf7195cc25d557e5028a9250d655c]
Former-commit-id: 64288f5a475a82d5f88c91347a09aea67ebb169d
2019-01-07 20:34:44 +00:00
Henrique Dias
77e1fe83db feat: wrap commands to send info (#612)
Wrap commands in a better way to pass storage

Former-commit-id: 9a3790c193936b53fe3826d1e051795efd30670b [formerly 04a9f34933a162cf4a98bc1e019f0d6c6404aae7] [formerly ab9be7f27b55a94e6a0f053df6908e357d4b396c [formerly 01ff03e426]]
Former-commit-id: ae7b61281dec2b5527f6032dc6f8c1bd8bb51296 [formerly 780ccc3b166e72bf53114bbfc0a95c0b7ffd8656]
Former-commit-id: 68ee8f7f3931f8e571b0188c96951077d6529cd7
2019-01-07 20:24:23 +00:00
Henrique Dias
1133f0f826 feat: rename import to upgrade
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 0c8eb7bea0cdf9745385fae660532bda9368c7be [formerly 5fb38e8b21346fa41fe0602929cd4f7f6fa19fa2] [formerly 9f39ca06f86dcb7c64b69ed4227789581f140397 [formerly f396602084]]
Former-commit-id: f310a5b4d7b1a54026dba8ccb637bf2ad5bf531a [formerly da06cb70d42ae5c2a4b0927f661b0026ae972792]
Former-commit-id: 23d0481a4cb60a70ab1b59bd8042c3923d161917
2019-01-07 19:22:32 +00:00
Henrique Dias
981b42fb5c fix: remove init from root
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 0a29d1eb40c1df0ad039959d3dc3ab4e5ebe328f [formerly eda99299d47c005f30bc04123403d9a739fd4933] [formerly f0703faa441e10b681854c71e6fb9fdbbad6ac44 [formerly 148d233d7a]]
Former-commit-id: e2162f9ac885704fc60e0b427b8331207b960712 [formerly c39d1dbdb49c5f534c69d6359f3afc793bcf5632]
Former-commit-id: de358cd75d688ba93fd4f36b8c5f3aadbe58ca3f
2019-01-07 19:21:29 +00:00
Henrique Dias
c59d42b601 feat: improve docs output
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: af19a86cebfe955619d5e05cfd5e9c4101a328e0 [formerly 84df35184bd684e00617651b0115b219e6e9c88e] [formerly 697906b1c6c4e47a03518edd50f5e3d86ea16690 [formerly ebaed3c390]]
Former-commit-id: a89ca0fd17984ebf6e95596ed515ad1592d314b6 [formerly e776a061d2628054eca107076601703aa2c7575d]
Former-commit-id: 7c809138219ef4723ec1ae47252eacdb60f7d78d
2019-01-06 18:36:47 +00:00
Henrique Dias
f70fc2d954 Merge pull request #610 from filebrowser/hashedpwd
feat: require hashed password and add hash command
Former-commit-id: b8d4bd7c9fbdde724beb62c77cee1daba15d315b [formerly fadf295f423c8979fcf7a704b05cffc3a0735d2a] [formerly e4a9044cd0a94b4f4fa1a8f441c563fe0a564a62 [formerly a2d32438f1]]
Former-commit-id: fbf8a1d1e85646a43bf220f1bb73d40481da7df0 [formerly a5b48143fb9c39da6b1b8f43c1e80729892a1837]
Former-commit-id: 5ba56215decd17c9c2b8cd59546a7181cef96aea
2019-01-06 18:25:09 +00:00
Henrique Dias
1aec3744d7 feat: only hash if it's default pw
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: f25a675cd60a328715febb7e6e799b28e3dbadec [formerly 09fc814b175a9d6ce7669daaaf64071c9de8d951] [formerly d53b7af3501535f25e74aef463c4b9d821486aa4 [formerly 550269d2ed]]
Former-commit-id: c21b1ecc3bdf6b1f8c9de8e18ba8d9a2777f0b27 [formerly a31b1e0ae18bc0bf69e83f564dfd50fb6b2c38b6]
Former-commit-id: fc3ee85d741cd83eda2961bcff91fb9d5ebc42d1
2019-01-06 17:55:47 +00:00
Henrique Dias
25792232e6 feat: add hash command
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 4d1b43927775194e96b8a9a3ae13fbffc21df572 [formerly b738daa5a48369b1fd5140a70b8b8965a34109e4] [formerly 5d2eefed13d38efdb480aab2d61a2e06244919eb [formerly a2742dff16]]
Former-commit-id: c4c6b087a91ea3d781fd5e5bbf9c511a9c25a312 [formerly 0aedca54ba1e98ad63e863fdd86e50123ad9078f]
Former-commit-id: b4581ad7c366baf486fc1412eaac4c6f9940b979
2019-01-06 17:53:47 +00:00
Henrique Dias
2869570a20 docs: update caddy issue template
Former-commit-id: af5dd2ab18970ed2a2792eb2bdb061d1540a67d3 [formerly e4563af635b2fe1d6d8abc6bcd07f120378f1cba] [formerly 633630c52d61cde8b8d5b31096a5621fa4063e84 [formerly 56c1536693]]
Former-commit-id: c58ffbae57cf39c342c5ce8bc6ae73b765edecf0 [formerly d4981d9243a139b6a96d791ef89f03c33a830b6d]
Former-commit-id: 01a668e7d5644566257d4f689928e5194ab5e59a
2019-01-06 16:39:59 +00:00
Henrique Dias
3a4ff6dbb2 Merge pull request #606 from filebrowser/viper
feat: dockerfile & viper
Former-commit-id: e819374dcd28bd939b0d3105f338a51cadf112fd [formerly e2886cb88a7642e9c681cd21205fd6f178e782df] [formerly 776586c1e92a0adcd0797a6ec8f95d7a0c177e65 [formerly 143dcec6f7]]
Former-commit-id: 8160ef39bcc5e01ff1489523240de0b59beb5bf9 [formerly 19520a57cc82f85178a5de7feac00cea3848791b]
Former-commit-id: 67518c991c6c4c3a6a8e8ffa5762001f01536013
2019-01-06 15:27:23 +00:00
Henrique Dias
42134a4849 feat: cleanup cli
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 2923c251335301f361098890bf67d47cd58c0f86 [formerly 277653e21c4b9077ea3b83c149f0c5b2982b694f] [formerly 7481557b6f47c8de6499f3ee7339ea8d29216700 [formerly 999c69de5c]]
Former-commit-id: 65eaacb4aee11f84c9f97ca2834def045921202a [formerly 7c9260a1fe142258dd0ac02c4710b03badd66d76]
Former-commit-id: ff3f3d7189b2e8cc2f00bef581cba108780e4552
2019-01-06 15:26:46 +00:00
Henrique Dias
e5c150e83f merge with master
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: dfac33db9ca3454cd3af02a732a64a02a8268b01 [formerly 38be015f7b3376a3bc992725c1cb1a019c1843cb] [formerly 5a4e310b96ebc5895e64f8eeb2f592890d4b63b0 [formerly 2b8bd28158]]
Former-commit-id: a30bfcebe6bbc115990b8f8398171b32942a8767 [formerly 553fafa22b46b2689ece093f3b0cf85b3cff1c87]
Former-commit-id: 5ce047d4e3f5217a179685f8b2850d4eb157376e
2019-01-06 13:20:17 +00:00
Henrique Dias
f1a89f5ec4 feat: add global scope (#604)
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: a44ebfc7a5687b5f51f0ff791335f66ab9f2e8e0 [formerly 9a044fadd8f2ebbb7dbb773273799c26a797f513] [formerly 7f374d016eaf756cfce215dd16a2274bbabe1915 [formerly f55f205ced]]
Former-commit-id: 31015ddc5f4fc28c895743f6fe9dcf5488bb4b01 [formerly e439027304a1e49667fafde011e07d043ef0d2ee]
Former-commit-id: 0394c60358673b56991364260b1cbe41fa457593
2019-01-06 13:01:42 +00:00
Henrique Dias
f2d952bf0e fix: dont fail when can't detect file type (#609)
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>
Former-commit-id: 3dbe943b6d64bf7d3f6c6d8970d2608438e4c2bc [formerly d5238a25f0e31dee84367a5e27b6f68ab50050d6] [formerly 297d8be6b3aa5e5411634286ba4faff256fd6837 [formerly 07f3ee38e5]]
Former-commit-id: 5a568c9eff47b5fa54d3956ecc2d2a55cb31fc8f [formerly 8d7f78cbf47a8a84f02324fdd858782e4b67b0e0]
Former-commit-id: 1343ba84d9c8952bd9e210cf7f23b70129236c81
2019-01-06 12:59:53 +00:00
Henrique Dias
70733ff846 feat: change default to 8080
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 4bcba1cc1d4f275c68b1e3a32045f6ec1136d264 [formerly 990ef78e797dc4a9828a6dd8c5c7162e06c6af3f] [formerly 1a4f246dc04fc76b0dd9656beb010d03fcef8045 [formerly e22598a126]]
Former-commit-id: 649df28a7201a0fefbd57b6faf56046fbf46c7b5 [formerly 7c763bae4fb9072920cc0907102583e15d1a7afc]
Former-commit-id: 163cfb1164e3f2be099722b013598fd9aba6611a
2019-01-06 12:57:35 +00:00
Henrique Dias
94619341f2 feat: simplify future changes
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 1fef9a9903b88ce216c4863a2c13d18030883f01 [formerly ab968c3e733db147d0e185934f03e7f965e03fde] [formerly a8e8e77ed08ca600abc156ba1cd57aff1af9d5c5 [formerly 12eb050cc2]]
Former-commit-id: eeb1864fb4c2c0d981c657a54c2d1e2a83a260e7 [formerly 09d661a1172e93b8e6a627dff2870b1e1c5903c6]
Former-commit-id: 5974a6e8e1309855e58160e6fa827de6dc5b9605
2019-01-06 12:41:52 +00:00
Henrique Dias
36bd2907f8 Merge pull request #608 from filebrowser/dont-persist
feat: don't persist server data on database
Former-commit-id: 768c9ce33d1f24626f94f1de558624382ebb4d97 [formerly de47b48b656299e3e9918e302257b031fd73bc34] [formerly 0304ed3fe19248a7635c06db5367478e5c5ac7bc [formerly 02c34ef8be]]
Former-commit-id: a780a2b2f96b2650c35cd65e3ece26cc1c58eb41 [formerly 4cac4d8b2e52632711d50e59c07107d677ce1616]
Former-commit-id: 930d1650c381ce4e02c79d29b2d3bcd9062828b7
2019-01-06 12:29:31 +00:00
Henrique Dias
f879944346 feat: dont persist server data on database
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 2ba9d0a5d060380a73ec1b46901a6f16a5e985a1 [formerly 1cd1c62eaba87c166d8fe10baebfe27bce0448a0] [formerly 3d1721671fb760a4b99aa66fe7d5a8d00579b17c [formerly a8ff679ae3]]
Former-commit-id: ff84b7c9f8982292219b6c673ecbe60b231cb567 [formerly 2cf251aa108b3415e766fd05dd268ccd05f0ee1c]
Former-commit-id: f112fdbd48836c509981cb93f41a47ff0eff77eb
2019-01-06 12:26:48 +00:00
Henrique Dias
d821418bca feat: update frontend
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 3454ae3d39d1739b4c586f099442894cc64b4473 [formerly e2024dbdb835a464b91b843d73ffbbb5ca84d190] [formerly 5ed6bc9695aa06e86ad6c67afec1af85d332dacf [formerly 22f2287bc3]]
Former-commit-id: f1a65e8d41ce28cc9d9d849d14ae407e48c58535 [formerly 1000226a97885b3e0a88aa3f0afaa7901ad4166d]
Former-commit-id: ce067696dd15c98f472d8cbdd77e764a6a1efe0c
2019-01-06 12:01:44 +00:00
Henrique Dias
a21198be11 feat: update frontend
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: abe6d7d161c5623b479a0331bfb035bad0e3ac2b [formerly c7de783f07988d163a19f5c4f4274de029fbc4dd] [formerly 776a733a98306debc757ca1048233b24d7823987 [formerly d97bafb53b]]
Former-commit-id: d405dd0600ad15bc6956c417f8894b8c51efa805 [formerly e9499a12c629884dd194e9ea4867fb07f3500735]
Former-commit-id: 36df4acbca806758aa1d031fa1e9f4ae041e5830
2019-01-06 10:06:39 +00:00
Henrique Dias
70042f4489 fix: allow embedding into iframes (close #550)
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: fd99b98b820e6d7bfe0e5e2c7cb0371e19497c57 [formerly 9c306af4416794e5a2e0d9165960617cfeca81e1] [formerly 9267510624a99cc72cf29ba671cca797d2f441e3 [formerly 2d858e6738]]
Former-commit-id: 3004c1182c71f3eb094d2ce1f60e5ec4364d00f8 [formerly ff39e10768468f8eb2580fae4aed5f9242925dba]
Former-commit-id: 0f7df8698160a72d74d6bdac41573b8c9c48cc4f
2019-01-06 09:29:23 +00:00
Henrique Dias
3890a9a416 fix: do not read whole file on listings
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: c630ed523a9f599c3dd1eda5930baa020d0b9911 [formerly f89f524d2979e43d6c2fd99903552bfe89addbc1] [formerly ba05ce7e5c730a04681910d87378dcdf3acc809b [formerly 28b326ab1d]]
Former-commit-id: cc4832f376eb2e5d5c99dfdac9b1966448b4e48f [formerly f254f221fa44f016a8f22cb3414580bb3a57750b]
Former-commit-id: 66c66d82ade69e62d32e05f8350166b2cdc3bb12
2019-01-06 09:24:09 +00:00
Henrique Dias
a58b64085d Merge pull request #603 from filebrowser/feat-viper
Feat viper

Former-commit-id: 688b44305afd67a5c018c7147d7b92d74978594b [formerly 6ddcd63bb1b210403c6ec3175bc8081971b82a96] [formerly 637bfd056f36ace26e958444318f3cbfc921c41e [formerly 72402b0274]]
Former-commit-id: ac60c7fa2da5bd5baa0522e106bbbff81ea2525f [formerly 8502db4715fd84e41665d97dd257ba1b27bb1dfb]
Former-commit-id: be28f4eb5496598b825bbac154906a33988bb224
2019-01-06 08:09:26 +00:00
1138-4EB
90b7f4aaf6 [WIP] add viper to manage envvars and cfg files
Former-commit-id: 99d241dc790629791b67b7b2ce20a10341a1c1e7 [formerly 08e8410bffdb50134a2d6ccbe853b25d4c4d3fc6] [formerly f32b18d9acb42ee1988c49b7980a16a606cee142 [formerly 1080bfde68]]
Former-commit-id: 318c4f159697e896bcf824e274907c5c03046bf0 [formerly 82533ae817673dfab8d56533f15a9931c0c74c1e]
Former-commit-id: ece42aa8e8c7480a5a21614b6ca397b71a01e1fa
2019-01-06 06:11:15 +01:00
1138-4EB
2def76df83 fix Dockerfile entrypoint args
Former-commit-id: 1569bdc5af20784b25dd1350dc640c99bb111736 [formerly 92e50eb0b6bc61b43f93515c79f7bf5741ce229f] [formerly 61cfe864e8dd0017060249b6962a5fa67bf1252f [formerly 09a6ffb1f3]]
Former-commit-id: 642e1ac56911282dfbea5e7c41044cc557d7c39f [formerly 8df377f2bda0c02c3b464529c871ccfb81fe968c]
Former-commit-id: 82ecd4ab3a9fce2c9ade430fc85417b9270c634a
2019-01-06 03:36:08 +01:00
Henrique Dias
1db210a24f 🧼: remove comment [ci skip]
Former-commit-id: 6560b8172709c5e8809b734eaf82139635c24905 [formerly c172a284c4c2bce5d7f82a13dda7cdc78d5c7d66] [formerly b75b0b28ef23b9e341628054252f32eb08f9933c [formerly c830b80269]]
Former-commit-id: b482c5c90f95166aaf3c368d136f23c67531d6a7 [formerly 52b0e3434d83a82f55aa6a0157a35578494b5aa0]
Former-commit-id: d1e5d7262a89f20f80274be277c9e388af56f20e
2019-01-05 23:56:22 +00:00
Henrique Dias
b0586d985c feat: scope must be set on CMD
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: 328b2a0ff52cb16c90158fff624f61230341b340 [formerly 68c35fd1abf023bdb9e1de101834f9b680c23dcf] [formerly 24554f91091c9d30073d4a98d462bea8d1928970 [formerly 3939e7b99d]]
Former-commit-id: ef97962090dad10d21924b476ec79b0fa92b906c [formerly f242696e812a57876edf282e0a7304860bd04b1f]
Former-commit-id: 85b816ed5ff5cf47ff2ae8a26f4a4cd99b1c29d6
2019-01-05 23:49:18 +00:00
Henrique Dias
a52374e288 feat: correct db path
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: c9c695d3b3c51a1e6f75b2319e52eb7a67221411 [formerly 4c1416815a2e2facf55f1fab9cb564b9090a305d] [formerly 09022439361037a8112f6421cc8eb9c6d5b93572 [formerly ed3d32e3ba]]
Former-commit-id: df28f382068ad0b6b1c4d2e2331ef035e54541d7 [formerly b7013cce5004ed0f8c0a157a8985949b11c73b35]
Former-commit-id: 49830926e40c5ac6bcbd9495a15c91ee6c1158c1
2019-01-05 23:48:51 +00:00
Henrique Dias
277f13a350 feat: update dockerfile
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>

Former-commit-id: e6b5d4dae3b1c92e5f45d1871d2cf522ebe231da [formerly d96f427f565caeaa73d496f1cfe5797849cbfee6] [formerly ecdfa88ca15f22287b8a145b1a32deac51febf54 [formerly 365eff8ca4]]
Former-commit-id: 7035068a2d4532448ce5f99df3e44a56ef70abf0 [formerly 22078ce93001feb79a7fe835ab33298287ffb23b]
Former-commit-id: 4d4576f98a9d992501e4ee7cd7d90a7dd319fabc
2019-01-05 23:47:56 +00:00
Henrique Dias
ea434ffc04 🧹: gofmt
Former-commit-id: 9548d370497e13d436abbc670cf40d360badab9f [formerly f29ba9c4dcf9962c3a5045629526296cd2b43ad8] [formerly 3f89c094c1b3330c8d6f1b2379d6309567d12210 [formerly 92fda0070c]]
Former-commit-id: 4ad64211019e0236f0eaa6e041a8793e899954fe [formerly 6bf71ffba205b563ff6cd0c7487d9a328c757686]
Former-commit-id: 4c7afe072fc3e2a225ca5a7dbf3f3beb66b44058
2019-01-05 23:01:16 +00:00
Henrique Dias
12b2c21522 feat: v2 (#599)
Read https://github.com/filebrowser/filebrowser/pull/575.

Former-commit-id: 7aedcaaf72b863033e3f089d6df308d41a3fd00c [formerly bdbe4d49161b901c4adf9c245895a1be2d62e4a7] [formerly acfc1ec67c423e0b3e065a8c1f8897c5249af65b [formerly d309066def]]
Former-commit-id: 0c7d925a38a68ccabdf2c4bbd8c302ee89b93509 [formerly a6173925a1382955d93b334ded93f70d6dddd694]
Former-commit-id: e032e0804dd051df86f42962de2b39caec5318b7
2019-01-05 22:44:33 +00:00
1138-4EB
53a4601361 chore: setting untracked version [ci skip]
Former-commit-id: 08352e25250ae60eca3289b81227833cd59c6122 [formerly f05e75cd9d0a34dc1155a348c26c6f1ce16d5973] [formerly 60bc13d70b80580426a7eee48a83efa4da16fdd8 [formerly 013ddf45c7]]
Former-commit-id: 591b084ed5dd5ebb0b5661f6bbad33dc5a19fec1 [formerly d3ca27f95cec8bd4b2557eb807699ae801b14804]
Former-commit-id: 993c87748dfd43ab90bed637a21bd008713299ba
2019-01-05 22:26:41 +01:00
127 changed files with 5950 additions and 4708 deletions

8
.docker.json Normal file
View File

@@ -0,0 +1,8 @@
{
"port": 80,
"baseURL": "",
"address": "",
"log": "stdout",
"database": "/database.db",
"root": "/srv"
}

View File

@@ -3,31 +3,4 @@ name: Caddy related bug report
about: Create a report to help us improve
---
**Are you asking for help with using Caddy or File Browser?**
Please use our forum instead: https://forum.caddyserver.com.
**When did you download File Browser from caddyserver.com?**
**What is your entire Caddyfile?**
```text
(Put Caddyfile here)
```
**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.
### Please open the issue on https://github.com/filebrowser/caddy/issues/new

17
.gitignore vendored
View File

@@ -1,15 +1,6 @@
.DS_Store
*/dist/*
*.db
*.db.lock
.idea
.vscode
Dockerfile
filebrowser
*.lock
*.bak
_old
rice-box.go
vendor
npm-debug.log*
package-lock.json
yarn-debug.log*
yarn-error.log*
yarn.lock
.idea/

View File

@@ -3,7 +3,7 @@ project_name: filebrowser
build:
env:
- CGO_ENABLED=0
main: cli/main.go
main: main.go
binary: filebrowser
goos:
- darwin
@@ -50,10 +50,9 @@ dockers:
goos: linux
goarch: amd64
goarm: ''
image: filebrowser/filebrowser
image_templates:
- "filebrowser/filebrowser:latest"
- "filebrowser/filebrowser:{{ .Tag }}"
skip_push: true
tag_templates:
- "{{ .Tag }}"
- latest
extra_files:
- Docker.json
- .docker.json

View File

@@ -5,37 +5,33 @@ install: skip
addons:
apt:
packages:
- docker-ce
- pass
env:
global:
- USE_DOCKER="true"
stages:
- lint
- test
- build
- release
cache:
directories:
- lib/rice-box.go
- http/rice-box.go
jobs:
include:
- stage: lint
script: "./build/run_linters.sh"
- stage: test
script: "./build/build_all.sh"
script: ./wizard.sh -l
- stage: build
script: ./wizard.sh -b
deploy:
provider: script
skip_cleanup: true
script: docker build -t filebrowser/filebrowser . && ./build/docker_login.sh && docker push filebrowser/filebrowser && docker logout
script: ./wizard.sh -p
on:
tags: false
repo: filebrowser/filebrowser
branch: master
- stage: release
script:
- docker run --rm -itv $(pwd):/src -w /src -v /var/run/docker.sock:/var/run/docker.sock filebrowser/dev sh -c "go get ./... && goreleaser"
- ./build/push_images.sh
- ./build/push_ricebox.sh
script: ./wizard.sh -r "$TRAVIS_TAG"
if: tag IS present
deploy:
provider: releases

View File

@@ -1,12 +0,0 @@
{
"port": 80,
"address": "",
"database": "/database.db",
"defaults": {
"scope": "/srv",
"allowCommands": true,
"allowEdit": true,
"allowNew": true,
"commands": []
}
}

View File

@@ -2,11 +2,10 @@ FROM scratch
COPY --from=filebrowser/dev /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
VOLUME /tmp
VOLUME /srv
EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
COPY Docker.json /.filebrowser.json
ENTRYPOINT ["/filebrowser"]
ENTRYPOINT [ "/filebrowser" ]

View File

@@ -1,16 +1,8 @@
INFO: **This project is not under active development ATM. A small group of developers keeps the project alive, but due to lack of time, we can't continue adding new features or doing deep changes. Please read [#532](https://github.com/filebrowser/filebrowser/issues/532) for more info!**
INFO: in Q2 2018, this project was renamed from `filemanager` to `filebrowser`, and the main repo was moved from [hacdias/filemanager](https://github.com/hacdias/filemanager) to [filebrowser/filebrowser](https://github.com/filebrowser/filebrowser). At the same time, the official docker image was changed to [`filebrowser/filebrowser`](https://hub.docker.com/r/filebrowser/filebrowser/). Users are encouraged to check their sources and update them accordingly.
---
<p align="center">
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
</p>
![Preview](https://user-images.githubusercontent.com/5447088/28537288-39be4288-70a2-11e7-8ce9-0813d59f46b7.gif)
# filebrowser
![Preview](https://user-images.githubusercontent.com/5447088/50716739-ebd26700-107a-11e9-9817-14230c53efd2.gif)
[![Travis](https://img.shields.io/travis/com/filebrowser/filebrowser.svg?style=flat-square)](https://travis-ci.com/filebrowser/filebrowser)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser?style=flat-square)](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
@@ -18,69 +10,22 @@
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg?style=flat-square)](https://github.com/filebrowser/filebrowser/releases/latest)
[![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23filebrowser)
> INFO: **This project is not under active development ATM. A small group of developers keeps the project alive, but due to lack of time, we can't continue adding new features or doing deep changes. Please read [#532](https://github.com/filebrowser/filebrowser/issues/532) for more info!**
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.
# Table of contents
## Features
+ [Getting started](#getting-started)
+ [Features](#features)
- [Users](#users)
- [Search](#search)
+ [Contributing](#contributing)
+ [Donate](#donate)
Please refer to our docs at [docs.filebrowser.xyz/features](https://docs.filebrowser.xyz/features)
# Getting started
## Install
You can find the Getting Started guide on the [documentation](https://filebrowser.github.io/quick-start/).
Please refer to our docs at [docs.filebrowser.xyz](https://docs.filebrowser.xyz/).
# Features
## Usage
Easy login system.
Please refer to our docs at [docs.filebrowser.xyz/usage](https://docs.filebrowser.xyz/usage).
![Login Page](https://user-images.githubusercontent.com/5447088/42046516-fe702976-7af5-11e8-9d72-c996150b09f5.png)
## Contributing
Listings of your files, available in two styles: mosaic and list. You can delete, move, rename, upload and create new files, as well as directories. Single files can be downloaded directly, and multiple files as *.zip*, *.tar*, *.tar.gz*, *.tar.bz2* or *.tar.xz*.
![Mosaic Listing](https://user-images.githubusercontent.com/5447088/42046515-fe3f7d58-7af5-11e8-8f87-270947ed755f.png)
File Browser editor is powered by [Codemirror](https://codemirror.net/) and if you're working with markdown files with metadata, both parts will be separated from each other so you can focus on the content.
![Markdown Editor](https://user-images.githubusercontent.com/5447088/42046519-ff17b81c-7af5-11e8-90f3-184e0ad24b7c.png)
On the settings page, a regular user can set its own custom CSS to personalize the experience and change its password. For admins, they can manage the permissions of each user, set commands which can be executed when certain events are triggered (such as before saving and after saving) and change plugin's settings.
![Settings](https://user-images.githubusercontent.com/5447088/42046517-fea206e4-7af5-11e8-88fe-b88513b43f43.png)
We also allow the users to search in the directories and execute commands if allowed.
## Users
We support multiple users and each user can have its own scope and custom stylesheet. The administrator is able to choose which permissions should be given to the users, as well as the commands they can execute. Each user also have a set of rules, in which he can be prevented or allowed to access some directories (regular expressions included!).
![Users](https://user-images.githubusercontent.com/5447088/42046518-fed14440-7af5-11e8-9a57-f4a611e9598d.png)
## Search
File Browser allows you to search through your files and it has some options. By default, your search will be something like this:
```
this are keywords
```
If you search for that it will look at every file that contains "this", "are" or "keywords" on their name. If you want to search for an exact term, you should surround your search by double quotes:
```
"this is the name"
```
That will search for any file that contains "this is the name" on its name. It won't search for each separated term this time.
By default, every search will be case insensitive. Although, you can make a case sensitive search by adding `case:sensitive` to the search terms, like this:
```
this are keywords case:sensitive
```
# Contributing
The contributing guidelines can be found [here](https://github.com/filebrowser/community).
Please refer to our docs at [docs.filebrowser.xyz/contributing](https://docs.filebrowser.xyz/contributing).

15
auth/auth.go Normal file
View File

@@ -0,0 +1,15 @@
package auth
import (
"net/http"
"github.com/filebrowser/filebrowser/v2/users"
)
// 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)
// LoginPage indicates if this auther needs a login page.
LoginPage() bool
}

107
auth/json.go Normal file
View File

@@ -0,0 +1,107 @@
package auth
import (
"encoding/json"
"net/http"
"net/url"
"os"
"strings"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// MethodJSONAuth is used to identify json auth.
const MethodJSONAuth settings.AuthMethod = "json"
type jsonCred struct {
Password string `json:"password"`
Username string `json:"username"`
ReCaptcha string `json:"recaptcha"`
}
// JSONAuth is a json implementaion of an Auther.
type JSONAuth struct {
ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"`
}
// 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) {
var cred jsonCred
if r.Body == nil {
return nil, os.ErrPermission
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
return nil, os.ErrPermission
}
// If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha)
if err != nil {
return nil, err
}
if !ok {
return nil, os.ErrPermission
}
}
u, err := sto.Get(root, cred.Username)
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
return nil, os.ErrPermission
}
return u, nil
}
// LoginPage tells that json auth doesn't require a login page.
func (a JSONAuth) LoginPage() bool {
return true
}
const reCaptchaAPI = "/recaptcha/api/siteverify"
// ReCaptcha identifies a recaptcha conenction.
type ReCaptcha struct {
Host string `json:"host"`
Key string `json:"key"`
Secret string `json:"secret"`
}
// Ok checks if a reCaptcha responde is correct.
func (r *ReCaptcha) Ok(response string) (bool, error) {
body := url.Values{}
body.Set("secret", r.Key)
body.Add("response", response)
client := &http.Client{}
resp, err := client.Post(
r.Host+reCaptchaAPI,
"application/x-www-form-urlencoded",
strings.NewReader(body.Encode()),
)
if err != nil {
return false, err
}
if resp.StatusCode != http.StatusOK {
return false, nil
}
var data struct {
Success bool `json:"success"`
}
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return false, err
}
return data.Success, nil
}

24
auth/none.go Normal file
View File

@@ -0,0 +1,24 @@
package auth
import (
"net/http"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// MethodNoAuth is used to identify no auth.
const MethodNoAuth settings.AuthMethod = "noauth"
// NoAuth is no auth implementation of auther.
type NoAuth struct{}
// Auth uses authenticates user 1.
func (a NoAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
return sto.Get(root, uint(1))
}
// LoginPage tells that no auth doesn't require a login page.
func (a NoAuth) LoginPage() bool {
return false
}

34
auth/proxy.go Normal file
View File

@@ -0,0 +1,34 @@
package auth
import (
"net/http"
"os"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// MethodProxyAuth is used to identify no auth.
const MethodProxyAuth settings.AuthMethod = "proxy"
// ProxyAuth is a proxy implementation of an auther.
type ProxyAuth struct {
Header string `json:"header"`
}
// Auth authenticates the user via an HTTP header.
func (a ProxyAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
username := r.Header.Get(a.Header)
user, err := sto.Get(root, username)
if err == errors.ErrNotExist {
return nil, os.ErrPermission
}
return user, err
}
// LoginPage tells that proxy auth doesn't require a login page.
func (a ProxyAuth) LoginPage() bool {
return false
}

33
auth/storage.go Normal file
View File

@@ -0,0 +1,33 @@
package auth
import (
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// StorageBackend is a storage backend for auth storage.
type StorageBackend interface {
Get(settings.AuthMethod) (Auther, error)
Save(Auther) error
}
// Storage is a auth storage.
type Storage struct {
back StorageBackend
users *users.Storage
}
// NewStorage creates a auth storage from a backend.
func NewStorage(back StorageBackend, users *users.Storage) *Storage {
return &Storage{back: back, users: users}
}
// Get wraps a StorageBackend.Get.
func (s *Storage) Get(t settings.AuthMethod) (Auther, error) {
return s.back.Get(t)
}
// Save wraps a StorageBackend.Save.
func (s *Storage) Save(a Auther) error {
return s.back.Save(a)
}

View File

@@ -1,20 +0,0 @@
#!/bin/sh
set -e
cd $(dirname $0)/../cli
go get -v ./...
if [ "$COMMIT_SHA" != "" ]; then
echo "Set version to ($COMMIT_SHA)"
sed -i.bak "s|(untracked)|($COMMIT_SHA)|g" ../lib/filebrowser.go
fi
echo "Build CLI"
go build -a -o filebrowser
if [ "$COMMIT_SHA" != "" ]; then
echo "Reset version to (untracked)"
sed -i "s|($COMMIT_SHA)|(untracked)|g" ../lib/filebrowser.go
fi

View File

@@ -1,37 +0,0 @@
#!/bin/sh
cd $(dirname $0)/..
if [ -d lib/"rice-box.go" ]; then
rm -rf lib/rice-box.go
fi
if [ "$USE_DOCKER" != "" ]; then
if [ -d "frontend/dist" ]; then
rm -rf frontend/dist
fi;
if [ "$(command -v git)" != "" ]; then
COMMIT_SHA="$(git rev-parse HEAD | cut -c1-8)"
else
COMMIT_SHA="untracked"
fi
$(command -v winpty) docker run --rm -it \
-v /$(pwd):/src:z \
-w //src \
-e COMMIT_SHA=$COMMIT_SHA \
filebrowser/dev \
sh -c "\
cd build && \
dos2unix build_assets.sh && \
dos2unix build.sh && \
./build_assets.sh && \
./build.sh &&
mv ../cli/filebrowser ../ \
"
else
set -e
./build/build_assets.sh
./build/build.sh
fi

View File

@@ -1,23 +0,0 @@
#!/bin/sh
set -e
cd $(dirname $0)/..
# Clean the dist folder and build the assets
cd frontend
if [ -d "dist" ]; then
rm -rf dist/*
fi;
yarn install
yarn build
cd ..
# Install rice tool if not present
if ! [ -x "$(command -v rice)" ]; then
go get github.com/GeertJohan/go.rice/rice
fi
# Embed the assets using rice
cd lib
rice embed-go

View File

@@ -1,27 +0,0 @@
#!/bin/sh
set -e
# init key for pass
gpg --batch --gen-key <<-EOF
%echo Generating a standard key
Key-Type: DSA
Key-Length: 1024
Subkey-Type: ELG-E
Subkey-Length: 1024
Name-Real: Meshuggah Rocks
Name-Email: meshuggah@example.com
Expire-Date: 0
# Do a commit here, so that we can later print "done" :-)
%commit
%echo done
EOF
key=$(gpg --no-auto-check-trustdb --list-secret-keys | grep ^sec | cut -d/ -f2 | cut -d" " -f1)
pass init $key
if [ "$(command -v docker-credential-pass)" = "" ]; then
docker run --rm -itv /usr/local/bin:/src filebrowser/dev sh -c "cp /go/bin/docker-credential-pass /src"
fi
echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin

View File

@@ -1,14 +0,0 @@
#! /bin/sh
set -e
cd $(dirname $0)
./docker_login.sh
for tag in `echo $(docker images filebrowser/filebrowser* | awk -F ' ' '{print $1 ":" $2}') | cut -d ' ' -f2-`; do
if [ "$tag" = "REPOSITORY:TAG" ]; then break; fi
docker push $tag
done
docker logout

View File

@@ -1,39 +0,0 @@
#!/bin/sh
set -e
cd $(dirname $0)
COMMIT_SHA="$(git rev-parse --verify HEAD | cut -c1-8)"
eval `ssh-agent -s`
openssl aes-256-cbc -K $encrypted_9ca81b5594f5_key -iv $encrypted_9ca81b5594f5_iv -in ./deploy_key.enc -d | ssh-add -
git clone git@github.com:filebrowser/caddy caddy
cd caddy
cp ../../lib/rice-box.go assets/
sed -i 's/package lib/package assets/g' assets/rice-box.go
git checkout -b update-rice-box origin/master
git config --local user.name "Filebrowser Bot"
git config --local user.email "FilebrowserBot@users.noreply.github.com"
git commit -am "update rice-box $COMMIT_SHA"
if [ $(git tag | grep "$TRAVIS_TAG" | wc -l) -ne 0 ]; then
git tag -d "$TRAVIS_TAG"
fi
git tag "$TRAVIS_TAG"
if [ "$(git ls-remote --heads origin update-rice-box)" != "" ]; then
git push -u origin update-rice-box
else
git push origin +update-rice-box
fi
if [ "$(git ls-remote --heads origin update-rice-box)" != "" ]; then
git push origin "$TRAVIS_TAG"
else
git push origin :"$TRAVIS_TAG"
git push origin "$TRAVIS_TAG"
fi

View File

@@ -1,55 +0,0 @@
#!/bin/bash
set -e
cd $(dirname $0)/..
echo "> Checking semver format"
if [ $# -ne 1 ]; then
echo "This release script requires a single argument corresponding to the semver to be released. See semver.org"
exit 1
fi
semver=$(grep -P '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)' <<< "$1")
if [ $? -ne 0 ]; then
echo "Not valid semver format. See semver.org"
exit 1
fi
echo "> Checking matching $semver in frontend submodule"
cd frontend
git fetch --all
if [ $(git tag | grep "$semver" | wc -l) -eq 0 ]; then
echo "Tag $semver does not exist in submodule 'frontend'. Tag it and run this script again."
exit 1
fi
git rev-parse --verify --quiet release
if [ $? -ne 0 ]; then
git checkout -b release "$semver"
else
git checkout release
git reset --hard "$semver"
fi
cd ..
echo "> Updating submodule ref to $semver"
sed -i "s|(untracked)|$1|g" filebrowser.go
git commit -am "chore: version $semver"
git tag "$1"
git push
git push --tags
echo "> Commiting untracked version notice..."
sed -i "s|$1|(untracked)|g" filebrowser.go
git commit -am "chore: setting untracked version [ci skip]"
git push
echo "> Done!"

View File

@@ -1,13 +0,0 @@
#!/bin/sh
set -e
cd $(dirname $0)/..
if [ "$USE_DOCKER" != "" ]; then
$(command -v winpty) docker run --rm -itv "/$(pwd)://src" -w "//src" filebrowser/dev sh -c "\
go get -v ./... && \
golangci-lint run -v"
else
golangci-lint run -v
fi

View File

@@ -1,24 +0,0 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// dbCmd represents the db command
var dbCmd = &cobra.Command{
Use: "db",
Version: rootCmd.Version,
Aliases: []string{"database"},
Short: "Manage a filebrowser database",
Long: `This is a CLI tool to ease the management of
filebrowser database files.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("db called. Command not implemented, yet.")
},
}
func init() {
rootCmd.AddCommand(dbCmd)
}

View File

@@ -1,76 +0,0 @@
package cmd
import (
"log"
"strings"
fb "github.com/filebrowser/filebrowser/lib"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
v "github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "filebrowser",
Version: fb.Version,
Aliases: []string{"serve"},
Short: "A stylish web-based file manager",
Long: `Command 'serve' is the default. Filebrowser is started
with the provided envvars, flags and/or config file. For example:
filebrowser -c config.json -p 80 -s ./srv
File Browser is a static binary composed of a golang backend and
a Vue.js frontend to create, edit, copy, move, download your files
easily, everywhere, every time.`,
// Run: func(cmd *cobra.Command, args []string) {},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
checkRootAlias()
if err := rootCmd.Execute(); err != nil {
panic(err)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SetVersionTemplate("File Browser {{printf \"version %s\" .Version}}\n")
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (defaults are './.filebrowser[ext]', '$HOME/.filebrowser[ext]' or '/etc/filebrowser/.filebrowser[ext]')")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile == "" {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
panic(err)
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
// Use config file from the flag.
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)
}
} else {
log.Println("Using config file:", v.ConfigFileUsed())
}
}

View File

@@ -1,46 +0,0 @@
package cmd
import (
"log"
"os"
)
// checkRootAlias compares the first argument provided in the CLI with a list of
// subcmds and aliases. If no match is found, the first alias of rootCmd is added.
func checkRootAlias() {
l := len(rootCmd.Aliases)
if l == 0 {
return
}
if l > 1 {
log.Printf("rootCmd.Aliases should contain a single string. '%s' is used.\n", rootCmd.Aliases[0])
}
if len(os.Args) > 1 {
for _, v := range append(nonRootSubCmds(), []string{"--help", "--version"}...) {
if os.Args[1] == v {
return
}
}
}
os.Args = append([]string{os.Args[0], rootCmd.Aliases[0]}, os.Args[1:]...)
}
// nonRootSubCmds traverses the list of subcommands of rootCmd and returns a string
// slice containing the names and aliases of all the subcmds, except the one defined
// in the Aliases field of rootCmd.
func nonRootSubCmds() (l []string) {
for _, c := range rootCmd.Commands() {
isAlias := false
for _, a := range append(c.Aliases, c.Name()) {
if a == rootCmd.Aliases[0] {
isAlias = true
break
}
}
if !isAlias {
l = append(l, c.Name())
l = append(l, c.Aliases...)
}
}
return
}

View File

@@ -1,111 +0,0 @@
package cmd
import (
filebrowser "github.com/filebrowser/filebrowser/lib"
"github.com/spf13/cobra"
v "github.com/spf13/viper"
)
// serveCmd represents the serve command
var serveCmd = &cobra.Command{
Use: "serve",
Version: rootCmd.Version,
Aliases: []string{"server"},
Short: "Start filebrowser service",
Long: rootCmd.Long,
Run: func(cmd *cobra.Command, args []string) {
Serve()
},
Args: cobra.NoArgs,
}
func init() {
rootCmd.AddCommand(serveCmd)
f := serveCmd.PersistentFlags()
flag := func(k string, i interface{}, u string) {
switch y := i.(type) {
case bool:
f.Bool(k, y, u)
case int:
f.Int(k, y, u)
case string:
f.String(k, y, u)
}
v.SetDefault(k, i)
}
flagP := func(k, p string, i interface{}, u string) {
switch y := i.(type) {
case bool:
f.BoolP(k, p, y, u)
case int:
f.IntP(k, p, y, u)
case string:
f.StringP(k, p, y, u)
}
v.SetDefault(k, i)
}
deprecated := func(k string, i interface{}, u, m string) {
switch y := i.(type) {
case bool:
f.Bool(k, y, u)
case int:
f.Int(k, y, u)
case string:
f.String(k, y, u)
}
f.MarkDeprecated(k, m)
}
// Global settings
flagP("port", "p", 0, "HTTP Port (default is random)")
flagP("address", "a", "", "Address to listen to (default is all of them)")
flagP("database", "d", "./filebrowser.db", "Database file")
flagP("log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file")
flagP("baseurl", "b", "", "Base URL")
flag("prefixurl", "", "Prefix URL")
flag("staticgen", "", "Static Generator you want to enable")
// User default settings
f.String("defaults.commands", "git svn hg", "Default commands option for new users")
v.SetDefault("defaults.commands", []string{"git", "svn", "hg"})
flagP("defaults.scope", "s", ".", "Default scope option for new users")
flag("defaults.viewMode", filebrowser.MosaicViewMode, "Default view mode for new users")
flag("defaults.allowCommands", true, "Default allow commands option for new users")
flag("defaults.allowEdit", true, "Default allow edit option for new users")
flag("defaults.allowNew", true, "Default allow new option for new users")
flag("defaults.allowPublish", true, "Default allow publish option for new users")
flag("defaults.locale", "", "Default locale for new users, set it empty to enable auto detect from browser")
// Recaptcha settings
flag("recaptcha.host", "https://www.google.com", "Use another host for ReCAPTCHA. recaptcha.net might be useful in China")
flag("recaptcha.key", "", "ReCaptcha site key")
flag("recaptcha.secret", "", "ReCaptcha secret")
// Auth settings
flag("auth.method", "default", "Switch between 'none', 'default' and 'proxy' authentication")
flag("auth.header", "X-Forwarded-User", "The header name used for proxy authentication")
// Bind the full flag set to the configuration
if err := v.BindPFlags(f); err != nil {
panic(err)
}
// Deprecated flags
deprecated("no-auth", false, "Disables authentication", "use --auth.method='none' instead")
deprecated("alternative-recaptcha", false, "Use recaptcha.net for serving and handling, useful in China", "use --recaptcha.host instead")
deprecated("recaptcha-key", "", "ReCaptcha site key", "use --recaptcha.key instead")
deprecated("recaptcha-secret", "", "ReCaptcha secret", "use --recaptcha.secret instead")
deprecated("scope", ".", "Default scope option for new users", "use --defaults.scope instead")
deprecated("commands", "git svn hg", "Default commands option for new users", "use --defaults.commands instead")
deprecated("view-mode", "mosaic", "Default view mode for new users", "use --defaults.viewMode instead")
deprecated("locale", "", "Default locale for new users, set it empty to enable auto detect from browser", "use --defaults.locale instead")
deprecated("allow-commands", true, "Default allow commands option for new users", "use --defaults.allowCommands instead")
deprecated("allow-edit", true, "Default allow edit option for new users", "use --defaults.allowEdit instead")
deprecated("allow-publish", true, "Default allow publish option for new users", "use --defaults.allowPublish instead")
deprecated("allow-new", true, "Default allow new option for new users", "use --defaults.allowNew instead")
}

View File

@@ -1,150 +0,0 @@
package cmd
import (
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"github.com/asdine/storm"
filebrowser "github.com/filebrowser/filebrowser/lib"
"github.com/filebrowser/filebrowser/lib/bolt"
h "github.com/filebrowser/filebrowser/lib/http"
"github.com/filebrowser/filebrowser/lib/staticgen"
"github.com/hacdias/fileutils"
"github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
)
func Serve() {
// Set up process log before anything bad happens.
switch l := viper.GetString("log"); l {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "":
log.SetOutput(ioutil.Discard)
default:
log.SetOutput(&lumberjack.Logger{
Filename: l,
MaxSize: 100,
MaxAge: 14,
MaxBackups: 10,
})
}
// Validate the provided config before moving forward
{
// Map of valid authentication methods, containing a boolean value to indicate the need of Auth.Header
validMethods := make(map[string]bool)
validMethods["none"] = false
validMethods["default"] = false
validMethods["proxy"] = true
m := viper.GetString("auth.method")
b, ok := validMethods[m]
if !ok {
log.Fatal("The property 'auth.method' needs to be set to 'none', 'default' or 'proxy'.")
}
if b {
if viper.GetString("auth.header") == "" {
log.Fatal("The 'auth.header' needs to be specified when '", m, "' authentication is used.")
}
log.Println("[WARN] Filebrowser authentication is configured to '", m, "' authentication. This can cause a huge security issue if the infrastructure is not configured correctly.")
}
}
// Builds the address and a listener.
laddr := viper.GetString("address") + ":" + viper.GetString("port")
listener, err := net.Listen("tcp", laddr)
if err != nil {
log.Fatal(err)
}
// Tell the user the port in which is listening.
log.Println("Listening on", listener.Addr().String())
// Starts the server.
if err := http.Serve(listener, handler()); err != nil {
log.Fatal(err)
}
}
func handler() http.Handler {
db, err := storm.Open(viper.GetString("database"))
if err != nil {
log.Fatal(err)
}
fb := &filebrowser.FileBrowser{
Auth: &filebrowser.Auth{
Method: viper.GetString("auth.method"),
Header: viper.GetString("auth.header"),
},
ReCaptcha: &filebrowser.ReCaptcha{
Host: viper.GetString("recaptcha.host"),
Key: viper.GetString("recaptcha.key"),
Secret: viper.GetString("recaptcha.secret"),
},
DefaultUser: &filebrowser.User{
AllowCommands: viper.GetBool("defaults.allowCommands"),
AllowEdit: viper.GetBool("defaults.allowEdit"),
AllowNew: viper.GetBool("defaults.allowNew"),
AllowPublish: viper.GetBool("defaults.allowPublish"),
Commands: viper.GetStringSlice("defaults.commands"),
Rules: []*filebrowser.Rule{},
Locale: viper.GetString("defaults.locale"),
CSS: "",
Scope: viper.GetString("defaults.scope"),
FileSystem: fileutils.Dir(viper.GetString("defaults.scope")),
ViewMode: viper.GetString("defaults.viewMode"),
},
Store: &filebrowser.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) filebrowser.FileSystem {
return fileutils.Dir(scope)
},
}
fb.SetBaseURL(viper.GetString("baseurl"))
fb.SetPrefixURL(viper.GetString("prefixurl"))
err = fb.Setup()
if err != nil {
log.Fatal(err)
}
switch viper.GetString("staticgen") {
case "hugo":
hugo := &staticgen.Hugo{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{},
CleanPublic: true,
}
if err = fb.Attach(hugo); err != nil {
log.Fatal(err)
}
case "jekyll":
jekyll := &staticgen.Jekyll{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"},
CleanPublic: true,
}
if err = fb.Attach(jekyll); err != nil {
log.Fatal(err)
}
}
return h.Handler(fb)
}

View File

@@ -1,28 +0,0 @@
package cmd
import (
"text/template"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of File Browser",
Long: `All software has versions. This is File Browser's`,
Run: func(cmd *cobra.Command, args []string) {
// https://github.com/spf13/cobra/issues/724
t := template.New("version")
template.Must(t.Parse(rootCmd.VersionTemplate()))
err := t.Execute(rootCmd.OutOrStdout(), rootCmd)
if err != nil {
rootCmd.Println(err)
}
},
}
func init() {
rootCmd.AddCommand(versionCmd)
serveCmd.AddCommand(versionCmd)
dbCmd.AddCommand(versionCmd)
}

View File

@@ -1,7 +0,0 @@
package main
import "github.com/filebrowser/filebrowser/cli/cmd"
func main() {
cmd.Execute()
}

12
cmd/cmd.go Normal file
View File

@@ -0,0 +1,12 @@
package cmd
import (
"log"
)
// Execute executes the commands.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}

26
cmd/cmds.go Normal file
View File

@@ -0,0 +1,26 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(cmdsCmd)
}
var cmdsCmd = &cobra.Command{
Use: "cmds",
Short: "Command runner management utility",
Long: `Command runner management utility.`,
Args: cobra.NoArgs,
}
func printEvents(m map[string][]string) {
for evt, cmds := range m {
for i, cmd := range cmds {
fmt.Printf("%s(%d): %s\n", evt, i, cmd)
}
}
}

27
cmd/cmds_add.go Normal file
View File

@@ -0,0 +1,27 @@
package cmd
import (
"strings"
"github.com/spf13/cobra"
)
func init() {
cmdsCmd.AddCommand(cmdsAddCmd)
}
var cmdsAddCmd = &cobra.Command{
Use: "add <event> <command>",
Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
command := strings.Join(args[1:], " ")
s.Commands[args[0]] = append(s.Commands[args[0]], command)
err = d.store.Settings.Save(s)
checkErr(err)
printEvents(s.Commands)
}, pythonConfig{}),
}

31
cmd/cmds_ls.go Normal file
View File

@@ -0,0 +1,31 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
cmdsCmd.AddCommand(cmdsLsCmd)
cmdsLsCmd.Flags().StringP("event", "e", "", "event name, without 'before' or 'after'")
}
var cmdsLsCmd = &cobra.Command{
Use: "ls",
Short: "List all commands for each event",
Long: `List all commands for each event.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
evt := mustGetString(cmd.Flags(), "event")
if evt == "" {
printEvents(s.Commands)
} else {
show := map[string][]string{}
show["before_"+evt] = s.Commands["before_"+evt]
show["after_"+evt] = s.Commands["after_"+evt]
printEvents(show)
}
}, pythonConfig{}),
}

56
cmd/cmds_rm.go Normal file
View File

@@ -0,0 +1,56 @@
package cmd
import (
"strconv"
"github.com/spf13/cobra"
)
func init() {
cmdsCmd.AddCommand(cmdsRmCmd)
}
var cmdsRmCmd = &cobra.Command{
Use: "rm <event> <index> [index_end]",
Short: "Removes a command from an event hooker",
Long: `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'.`,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil {
return err
}
for _, arg := range args[1:] {
if _, err := strconv.Atoi(arg); err != nil {
return err
}
}
return nil
},
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
evt := args[0]
i, err := strconv.Atoi(args[1])
checkErr(err)
f := i
if len(args) == 3 {
f, err = strconv.Atoi(args[2])
checkErr(err)
}
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
err = d.store.Settings.Save(s)
checkErr(err)
printEvents(s.Commands)
}, pythonConfig{}),
}

128
cmd/config.go Normal file
View File

@@ -0,0 +1,128 @@
package cmd
import (
"encoding/json"
nerrors "errors"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
rootCmd.AddCommand(configCmd)
}
var configCmd = &cobra.Command{
Use: "config",
Short: "Configuration management utility",
Long: `Configuration management utility.`,
Args: cobra.NoArgs,
}
func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags)
addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup")
flags.String("shell", "", "shell command to which other commands should be appended")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
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.secret", "", "ReCaptcha secret")
flags.String("branding.name", "", "replace 'File Browser' by this name")
flags.String("branding.files", "", "path to directory with images and custom styles")
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
}
func getAuthentication(flags *pflag.FlagSet) (settings.AuthMethod, auth.Auther) {
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
var auther auth.Auther
if method == auth.MethodProxyAuth {
header := mustGetString(flags, "auth.header")
if header == "" {
panic(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 != "" && 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) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Auth method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:")
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal)
fmt.Fprintln(w, "\nServer:")
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
fmt.Fprintf(w, "\tBase URL:\t%s\n", ser.BaseURL)
fmt.Fprintf(w, "\tRoot:\t%s\n", ser.Root)
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
fmt.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
fmt.Fprintf(w, "\tSorting:\n")
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, "\tPermissions:\n")
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\tCreate:\t%t\n", set.Defaults.Perm.Create)
fmt.Fprintf(w, "\t\tRename:\t%t\n", set.Defaults.Perm.Rename)
fmt.Fprintf(w, "\t\tModify:\t%t\n", set.Defaults.Perm.Modify)
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\tDownload:\t%t\n", set.Defaults.Perm.Download)
w.Flush()
b, err := json.MarshalIndent(auther, "", " ")
checkErr(err)
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
}

25
cmd/config_cat.go Normal file
View File

@@ -0,0 +1,25 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
configCmd.AddCommand(configCatCmd)
}
var configCatCmd = &cobra.Command{
Use: "cat",
Short: "Prints the configuration",
Long: `Prints the configuration.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
set, err := d.store.Settings.Get()
checkErr(err)
ser, err := d.store.Settings.GetServer()
checkErr(err)
auther, err := d.store.Auth.Get(set.AuthMethod)
checkErr(err)
printSettings(ser, set, auther)
}, pythonConfig{}),
}

37
cmd/config_export.go Normal file
View File

@@ -0,0 +1,37 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
configCmd.AddCommand(configExportCmd)
}
var configExportCmd = &cobra.Command{
Use: "export <path>",
Short: "Export the configuration to a file",
Long: `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.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
settings, err := d.store.Settings.Get()
checkErr(err)
server, err := d.store.Settings.GetServer()
checkErr(err)
auther, err := d.store.Auth.Get(settings.AuthMethod)
checkErr(err)
data := &settingsFile{
Settings: settings,
Auther: auther,
Server: server,
}
err = marshal(args[0], data)
checkErr(err)
}, pythonConfig{}),
}

91
cmd/config_import.go Normal file
View File

@@ -0,0 +1,91 @@
package cmd
import (
"encoding/json"
"errors"
"path/filepath"
"reflect"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/spf13/cobra"
)
func init() {
configCmd.AddCommand(configImportCmd)
}
type settingsFile struct {
Settings *settings.Settings `json:"settings"`
Server *settings.Server `json:"server"`
Auther interface{} `json:"auther"`
}
var configImportCmd = &cobra.Command{
Use: "import <path>",
Short: "Import a configuration file",
Long: `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.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
var key []byte
if d.hadDB {
settings, err := d.store.Settings.Get()
checkErr(err)
key = settings.Key
} else {
key = generateKey()
}
file := settingsFile{}
err := unmarshal(args[0], &file)
checkErr(err)
file.Settings.Key = key
err = d.store.Settings.Save(file.Settings)
checkErr(err)
err = d.store.Settings.SaveServer(file.Server)
checkErr(err)
var rawAuther interface{}
if filepath.Ext(args[0]) != ".json" {
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
} else {
rawAuther = file.Auther
}
var auther auth.Auther
switch file.Settings.AuthMethod {
case auth.MethodJSONAuth:
auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth)
case auth.MethodNoAuth:
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
case auth.MethodProxyAuth:
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
default:
checkErr(errors.New("invalid auth method"))
}
err = d.store.Auth.Save(auther)
checkErr(err)
printSettings(file.Server, file.Settings, auther)
}, pythonConfig{allowNoDB: true}),
}
func getAuther(sample auth.Auther, data interface{}) interface{} {
authType := reflect.TypeOf(sample)
auther := reflect.New(authType).Interface()
bytes, err := json.Marshal(data)
checkErr(err)
err = json.Unmarshal(bytes, &auther)
checkErr(err)
return auther
}

68
cmd/config_init.go Normal file
View File

@@ -0,0 +1,68 @@
package cmd
import (
"fmt"
"strings"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/spf13/cobra"
)
func init() {
configCmd.AddCommand(configInitCmd)
addConfigFlags(configInitCmd.Flags())
}
var configInitCmd = &cobra.Command{
Use: "init",
Short: "Initialize a new database",
Long: `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.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
defaults := settings.UserDefaults{}
flags := cmd.Flags()
getUserDefaults(flags, &defaults, true)
authMethod, auther := getAuthentication(flags)
s := &settings.Settings{
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
Shell: strings.Split(strings.TrimSpace(mustGetString(flags, "shell")), " "),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
Files: mustGetString(flags, "branding.files"),
},
}
ser := &settings.Server{
Address: mustGetString(flags, "address"),
Root: mustGetString(flags, "root"),
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)
checkErr(err)
err = d.store.Settings.SaveServer(ser)
checkErr(err)
err = d.store.Auth.Save(auther)
checkErr(err)
fmt.Printf(`
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
need to call the main command to boot up the server.
`)
printSettings(ser, s, auther)
}, pythonConfig{noDB: true}),
}

80
cmd/config_set.go Normal file
View File

@@ -0,0 +1,80 @@
package cmd
import (
"strings"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
configCmd.AddCommand(configSetCmd)
addConfigFlags(configSetCmd.Flags())
}
var configSetCmd = &cobra.Command{
Use: "set",
Short: "Updates the configuration",
Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
flags := cmd.Flags()
set, err := d.store.Settings.Get()
checkErr(err)
ser, err := d.store.Settings.GetServer()
checkErr(err)
hasAuth := false
flags.Visit(func(flag *pflag.Flag) {
switch flag.Name {
case "baseurl":
ser.BaseURL = mustGetString(flags, flag.Name)
case "root":
ser.Root = 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)
var auther auth.Auther
if hasAuth {
set.AuthMethod, auther = getAuthentication(flags)
err = d.store.Auth.Save(auther)
checkErr(err)
} else {
auther, err = d.store.Auth.Get(set.AuthMethod)
checkErr(err)
}
err = d.store.Settings.Save(set)
checkErr(err)
err = d.store.Settings.SaveServer(ser)
checkErr(err)
printSettings(ser, set, auther)
}, pythonConfig{}),
}

139
cmd/docs.go Normal file
View File

@@ -0,0 +1,139 @@
package cmd
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
rootCmd.AddCommand(docsCmd)
docsCmd.Flags().StringP("path", "p", "./docs", "path to save the docs")
}
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{
Use: "docs",
Hidden: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
dir := mustGetString(cmd.Flags(), "path")
generateDocs(rootCmd, dir)
names := []string{}
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
if !strings.HasPrefix(info.Name(), "filebrowser") {
return nil
}
names = append(names, info.Name())
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)
}
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 len(long) == 0 {
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, name)
_, err := buf.WriteTo(w)
checkErr(err)
}
func generateFlagsTable(fs *pflag.FlagSet, buf *bytes.Buffer) {
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, name string) {
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")
}
}

24
cmd/hash.go Normal file
View File

@@ -0,0 +1,24 @@
package cmd
import (
"fmt"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(hashCmd)
}
var hashCmd = &cobra.Command{
Use: "hash <password>",
Short: "Hashes a password",
Long: `Hashes a password using bcrypt algorithm.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
pwd, err := users.HashPwd(args[0])
checkErr(err)
fmt.Println(pwd)
},
}

310
cmd/root.go Normal file
View File

@@ -0,0 +1,310 @@
package cmd
import (
"crypto/tls"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/filebrowser/filebrowser/v2/auth"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
)
var (
cfgFile string
)
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
flags := rootCmd.Flags()
persistent := rootCmd.PersistentFlags()
persistent.StringVarP(&cfgFile, "config", "c", "", "config file 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\")")
addServerFlags(flags)
}
func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("address", "a", "127.0.0.1", "address to listen on")
flags.StringP("log", "l", "stdout", "log output")
flags.StringP("port", "p", "8080", "port to listen on")
flags.StringP("cert", "t", "", "tls certificate")
flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths")
flags.StringP("baseurl", "b", "", "base url")
}
var rootCmd = &cobra.Command{
Use: "filebrowser",
Short: "A stylish web-based 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
web interface.
If you've never run File Browser, you'll need to have a database for
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
by ourselves.
For this specific command, all the flags you have available (except
"config" for the configuration file), can be given either through
environment variables or configuration files.
If you don't set "config", it will look for a configuration file called
.filebrowser.{json, toml, yaml, yml} in the following directories:
- ./
- $HOME/
- /etc/filebrowser/
The precedence of the configuration values are as follows:
- flags
- environment variables
- configuration file
- database values
- 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
the quick setup mode and a new database will be bootstraped and a new
user created with the credentials from options "username" and "password".`,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
log.Println(cfgFile)
if !d.hadDB {
quickSetup(cmd.Flags(), d)
}
server := getRunParams(cmd.Flags(), d.store)
setupLog(server.Log)
root, err := filepath.Abs(server.Root)
checkErr(err)
server.Root = root
adr := server.Address + ":" + server.Port
var listener net.Listener
if server.TLSKey != "" && server.TLSCert != "" {
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
checkErr(err)
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}})
checkErr(err)
} else {
listener, err = net.Listen("tcp", adr)
checkErr(err)
}
handler, err := fbhttp.NewHandler(d.store, server)
checkErr(err)
log.Println("Listening on", listener.Addr().String())
if err := http.Serve(listener, handler); err != nil {
log.Fatal(err)
}
}, pythonConfig{allowNoDB: true}),
}
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server, err := st.Settings.GetServer()
checkErr(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, "address"); set {
server.Address = val
}
if val, set := getParamB(flags, "port"); set {
server.Port = val
}
if val, set := getParamB(flags, "log"); set {
server.Log = val
}
if val, set := getParamB(flags, "key"); set {
server.TLSKey = val
}
if val, set := getParamB(flags, "cert"); set {
server.TLSCert = val
}
return server
}
// getParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// 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 v.IsSet(key) {
return v.GetString(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getParam(flags *pflag.FlagSet, key string) string {
val, _ := getParamB(flags, key)
return val
}
func setupLog(logMethod string) {
switch logMethod {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "":
log.SetOutput(ioutil.Discard)
default:
log.SetOutput(&lumberjack.Logger{
Filename: logMethod,
MaxSize: 100,
MaxAge: 14,
MaxBackups: 10,
})
}
}
func quickSetup(flags *pflag.FlagSet, d pythonData) {
set := &settings.Settings{
Key: generateKey(),
Signup: false,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
Perm: users.Permissions{
Admin: false,
Execute: true,
Create: true,
Rename: true,
Modify: true,
Delete: true,
Share: true,
Download: true,
},
},
}
var err error
if _, noauth := getParamB(flags, "noauth"); noauth {
set.AuthMethod = auth.MethodNoAuth
err = d.store.Auth.Save(&auth.NoAuth{})
} else {
set.AuthMethod = auth.MethodJSONAuth
err = d.store.Auth.Save(&auth.JSONAuth{})
}
checkErr(err)
err = d.store.Settings.Save(set)
checkErr(err)
ser := &settings.Server{
BaseURL: getParam(flags, "baseurl"),
Port: getParam(flags, "port"),
Log: getParam(flags, "log"),
TLSKey: getParam(flags, "key"),
TLSCert: getParam(flags, "cert"),
Address: getParam(flags, "address"),
Root: getParam(flags, "root"),
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
username := getParam(flags, "username")
password := getParam(flags, "password")
if password == "" {
password, err = users.HashPwd("admin")
checkErr(err)
}
if username == "" || password == "" {
log.Fatal("username and password cannot be empty during quick setup")
}
user := &users.User{
Username: username,
Password: password,
LockPassword: false,
}
set.Defaults.Apply(user)
user.Perm.Admin = true
err = d.store.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()
}
}

65
cmd/rule_rm.go Normal file
View File

@@ -0,0 +1,65 @@
package cmd
import (
"strconv"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
)
func init() {
rulesCmd.AddCommand(rulesRmCommand)
rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove")
rulesRmCommand.MarkFlagRequired("index")
}
var rulesRmCommand = &cobra.Command{
Use: "rm <index> [index_end]",
Short: "Remove a global rule or user rule",
Long: `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'.`,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil {
return err
}
for _, arg := range args {
if _, err := strconv.Atoi(arg); err != nil {
return err
}
}
return nil
},
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
i, err := strconv.Atoi(args[0])
checkErr(err)
f := i
if len(args) == 2 {
f, err = strconv.Atoi(args[1])
checkErr(err)
}
user := func(u *users.User) {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
err := d.store.Users.Save(u)
checkErr(err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
err := d.store.Settings.Save(s)
checkErr(err)
}
runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

91
cmd/rules.go Normal file
View File

@@ -0,0 +1,91 @@
package cmd
import (
"fmt"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
rootCmd.AddCommand(rulesCmd)
rulesCmd.PersistentFlags().StringP("username", "u", "", "username of user to which the rules apply")
rulesCmd.PersistentFlags().UintP("id", "i", 0, "id of user to which the rules apply")
}
var rulesCmd = &cobra.Command{
Use: "rules",
Short: "Rules management utility",
Long: `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.`,
Args: cobra.NoArgs,
}
func runRules(st *storage.Storage, cmd *cobra.Command, users func(*users.User), global func(*settings.Settings)) {
id := getUserIdentifier(cmd.Flags())
if id != nil {
user, err := st.Users.Get("", id)
checkErr(err)
if users != nil {
users(user)
}
printRules(user.Rules, id)
return
}
s, err := st.Settings.Get()
checkErr(err)
if global != nil {
global(s)
}
printRules(s.Rules, id)
}
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
id := mustGetUint(flags, "id")
username := mustGetString(flags, "username")
if id != 0 {
return id
} else if username != "" {
return username
}
return nil
}
func printRules(rules []rules.Rule, id interface{}) {
if id == nil {
fmt.Printf("Global Rules:\n\n")
} else {
fmt.Printf("Rules for user %v:\n\n", id)
}
for id, rule := range rules {
fmt.Printf("(%d) ", id)
if rule.Regex {
if rule.Allow {
fmt.Printf("Allow Regex: \t%s\n", rule.Regexp.Raw)
} else {
fmt.Printf("Disallow Regex: \t%s\n", rule.Regexp.Raw)
}
} else {
if rule.Allow {
fmt.Printf("Allow Path: \t%s\n", rule.Path)
} else {
fmt.Printf("Disallow Path: \t%s\n", rule.Path)
}
}
}
}

57
cmd/rules_add.go Normal file
View File

@@ -0,0 +1,57 @@
package cmd
import (
"regexp"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
)
func init() {
rulesCmd.AddCommand(rulesAddCmd)
rulesAddCmd.Flags().BoolP("allow", "a", false, "indicates this is an allow rule")
rulesAddCmd.Flags().BoolP("regex", "r", false, "indicates this is a regex rule")
}
var rulesAddCmd = &cobra.Command{
Use: "add <path|expression>",
Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
allow := mustGetBool(cmd.Flags(), "allow")
regex := mustGetBool(cmd.Flags(), "regex")
exp := args[0]
if regex {
regexp.MustCompile(exp)
}
rule := rules.Rule{
Allow: allow,
Regex: regex,
}
if regex {
rule.Regexp = &rules.Regexp{Raw: exp}
} else {
rule.Path = exp
}
user := func(u *users.User) {
u.Rules = append(u.Rules, rule)
err := d.store.Users.Save(u)
checkErr(err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules, rule)
err := d.store.Settings.Save(s)
checkErr(err)
}
runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

19
cmd/rules_ls.go Normal file
View File

@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
rulesCmd.AddCommand(rulesLsCommand)
}
var rulesLsCommand = &cobra.Command{
Use: "ls",
Short: "List global rules or user specific rules",
Long: `List global rules or user specific rules.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
runRules(d.store, cmd, nil, nil)
}, pythonConfig{}),
}

30
cmd/upgrade.go Normal file
View File

@@ -0,0 +1,30 @@
package cmd
import (
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
"github.com/spf13/cobra"
)
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)
},
}

128
cmd/users.go Normal file
View File

@@ -0,0 +1,128 @@
package cmd
import (
"errors"
"fmt"
"os"
"strconv"
"text/tabwriter"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
rootCmd.AddCommand(usersCmd)
}
var usersCmd = &cobra.Command{
Use: "users",
Short: "Users management utility",
Long: `Users management utility.`,
Args: cobra.NoArgs,
}
func printUsers(users []*users.User) {
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")
for _, user := range users {
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",
user.ID,
user.Username,
user.Scope,
user.Locale,
user.ViewMode,
user.Perm.Admin,
user.Perm.Execute,
user.Perm.Create,
user.Perm.Rename,
user.Perm.Modify,
user.Perm.Delete,
user.Perm.Share,
user.Perm.Download,
user.LockPassword,
)
}
w.Flush()
}
func parseUsernameOrID(arg string) (string, uint) {
id, err := strconv.ParseUint(arg, 10, 0)
if err != nil {
return arg, 0
}
return "", uint(id)
}
func addUserFlags(flags *pflag.FlagSet) {
flags.Bool("perm.admin", false, "admin perm for users")
flags.Bool("perm.execute", true, "execute perm for users")
flags.Bool("perm.create", true, "create perm for users")
flags.Bool("perm.rename", true, "rename perm for users")
flags.Bool("perm.modify", true, "modify perm for users")
flags.Bool("perm.delete", true, "delete perm for users")
flags.Bool("perm.share", true, "share perm for users")
flags.Bool("perm.download", true, "download perm for users")
flags.String("sorting.by", "name", "sorting mode (name, size or modified)")
flags.Bool("sorting.asc", false, "sorting by ascending order")
flags.Bool("lockPassword", false, "lock password")
flags.StringSlice("commands", nil, "a list of the commands a user can execute")
flags.String("scope", ".", "scope for users")
flags.String("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
}
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
viewMode := users.ViewMode(mustGetString(flags, "viewMode"))
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\""))
}
return viewMode
}
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
visit := func(flag *pflag.Flag) {
switch flag.Name {
case "scope":
defaults.Scope = mustGetString(flags, flag.Name)
case "locale":
defaults.Locale = mustGetString(flags, flag.Name)
case "viewMode":
defaults.ViewMode = getViewMode(flags)
case "perm.admin":
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
case "perm.execute":
defaults.Perm.Execute = mustGetBool(flags, flag.Name)
case "perm.create":
defaults.Perm.Create = mustGetBool(flags, flag.Name)
case "perm.rename":
defaults.Perm.Rename = mustGetBool(flags, flag.Name)
case "perm.modify":
defaults.Perm.Modify = mustGetBool(flags, flag.Name)
case "perm.delete":
defaults.Perm.Delete = mustGetBool(flags, flag.Name)
case "perm.share":
defaults.Perm.Share = mustGetBool(flags, flag.Name)
case "perm.download":
defaults.Perm.Download = mustGetBool(flags, flag.Name)
case "commands":
commands, err := flags.GetStringSlice(flag.Name)
checkErr(err)
defaults.Commands = commands
case "sorting.by":
defaults.Sorting.By = mustGetString(flags, flag.Name)
case "sorting.asc":
defaults.Sorting.Asc = mustGetBool(flags, flag.Name)
}
}
if all {
flags.VisitAll(visit)
} else {
flags.Visit(visit)
}
}

37
cmd/users_add.go Normal file
View File

@@ -0,0 +1,37 @@
package cmd
import (
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersAddCmd)
addUserFlags(usersAddCmd.Flags())
}
var usersAddCmd = &cobra.Command{
Use: "add <username> <password>",
Short: "Create a new user",
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
getUserDefaults(cmd.Flags(), &s.Defaults, false)
password, err := users.HashPwd(args[1])
checkErr(err)
user := &users.User{
Username: args[0],
Password: password,
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
}
s.Defaults.Apply(user)
err = d.store.Users.Save(user)
checkErr(err)
printUsers([]*users.User{user})
}, pythonConfig{}),
}

24
cmd/users_export.go Normal file
View File

@@ -0,0 +1,24 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersExportCmd)
}
var usersExportCmd = &cobra.Command{
Use: "export <path>",
Short: "Export all users to a file.",
Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
list, err := d.store.Users.Gets("")
checkErr(err)
err = marshal(args[0], list)
checkErr(err)
}, pythonConfig{}),
}

50
cmd/users_find.go Normal file
View File

@@ -0,0 +1,50 @@
package cmd
import (
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersFindCmd)
usersCmd.AddCommand(usersLsCmd)
}
var usersFindCmd = &cobra.Command{
Use: "find <id|username>",
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.`,
Args: cobra.ExactArgs(1),
Run: findUsers,
}
var usersLsCmd = &cobra.Command{
Use: "ls",
Short: "List all users.",
Args: cobra.NoArgs,
Run: findUsers,
}
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
var (
list []*users.User
user *users.User
err error
)
if len(args) == 1 {
username, id := parseUsernameOrID(args[0])
if username != "" {
user, err = d.store.Users.Get("", username)
} else {
user, err = d.store.Users.Get("", id)
}
list = []*users.User{user}
} else {
list, err = d.store.Users.Gets("")
}
checkErr(err)
printUsers(list)
}, pythonConfig{})

87
cmd/users_import.go Normal file
View File

@@ -0,0 +1,87 @@
package cmd
import (
"errors"
"os"
"strconv"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersImportCmd)
usersImportCmd.Flags().Bool("overwrite", false, "overwrite users with the same id/username combo")
usersImportCmd.Flags().Bool("replace", false, "replace the entire user base")
}
var usersImportCmd = &cobra.Command{
Use: "import <path>",
Short: "Import users from a file",
Long: `Import users from a file. The path must be for a json or yaml
file. You can use this command to import new users to your
installation. For that, just don't place their ID on the files
list or set it to 0.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
fd, err := os.Open(args[0])
checkErr(err)
defer fd.Close()
list := []*users.User{}
err = unmarshal(args[0], &list)
checkErr(err)
for _, user := range list {
err = user.Clean("")
checkErr(err)
}
if mustGetBool(cmd.Flags(), "replace") {
oldUsers, err := d.store.Users.Gets("")
checkErr(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")
for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID)
// User exists in DB.
if err == nil {
if !overwrite {
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registred"))
}
// 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
// operation
if user.Username != onDB.Username {
conflictuous, err := d.store.Users.Get("", user.Username)
if err == nil {
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
}
}
} else {
// If it doesn't exist, set the ID to 0 to automatically get a new
// one that make sense in this DB.
user.ID = 0
}
err = d.store.Users.Save(user)
checkErr(err)
}
}, pythonConfig{}),
}
func usernameConflictError(username string, original, new uint) error {
return errors.New("can't import user with ID " + strconv.Itoa(int(new)) + " and username \"" + username + "\" because the username is already registred with the user " + strconv.Itoa(int(original)))
}

31
cmd/users_rm.go Normal file
View File

@@ -0,0 +1,31 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersRmCmd)
}
var usersRmCmd = &cobra.Command{
Use: "rm <id|username>",
Short: "Delete a user by username or id",
Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
username, id := parseUsernameOrID(args[0])
var err error
if username != "" {
err = d.store.Users.Delete(username)
} else {
err = d.store.Users.Delete(id)
}
checkErr(err)
fmt.Println("user deleted successfully")
}, pythonConfig{}),
}

72
cmd/users_update.go Normal file
View File

@@ -0,0 +1,72 @@
package cmd
import (
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersUpdateCmd)
usersUpdateCmd.Flags().StringP("password", "p", "", "new password")
usersUpdateCmd.Flags().StringP("username", "u", "", "new username")
addUserFlags(usersUpdateCmd.Flags())
}
var usersUpdateCmd = &cobra.Command{
Use: "update <id|username>",
Short: "Updates an existing user",
Long: `Updates an existing user. Set the flags for the
options you want to change.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
username, id := parseUsernameOrID(args[0])
flags := cmd.Flags()
password := mustGetString(flags, "password")
newUsername := mustGetString(flags, "username")
var (
err error
user *users.User
)
if id != 0 {
user, err = d.store.Users.Get("", id)
} else {
user, err = d.store.Users.Get("", username)
}
checkErr(err)
defaults := settings.UserDefaults{
Scope: user.Scope,
Locale: user.Locale,
ViewMode: user.ViewMode,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
}
getUserDefaults(flags, &defaults, false)
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
user.LockPassword = mustGetBool(flags, "lockPassword")
if newUsername != "" {
user.Username = newUsername
}
if password != "" {
user.Password, err = users.HashPwd(password)
checkErr(err)
}
err = d.store.Users.Update(user)
checkErr(err)
printUsers([]*users.User{user})
}, pythonConfig{}),
}

177
cmd/utils.go Normal file
View File

@@ -0,0 +1,177 @@
package cmd
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
yaml "gopkg.in/yaml.v2"
)
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}
func mustGetString(flags *pflag.FlagSet, flag string) string {
s, err := flags.GetString(flag)
checkErr(err)
return s
}
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
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 {
k, err := settings.GenerateKey()
checkErr(err)
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) {
stat, err := os.Stat(path)
if err == nil {
return stat.Size() != 0, nil
}
if os.IsNotExist(err) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
os.MkdirAll(d, 0700)
return false, nil
}
}
return false, err
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) {
data := pythonData{hadDB: true}
path := getParam(cmd.Flags(), "database")
exists, err := dbExists(path)
if err != nil {
panic(err)
} else if exists && cfg.noDB {
log.Fatal(path + " already exists")
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
}
data.hadDB = exists
db, err := storm.Open(path)
checkErr(err)
defer db.Close()
data.store, err = bolt.NewStorage(db)
checkErr(err)
fn(cmd, args, data)
}
}
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
checkErr(err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
case ".json":
encoder := json.NewEncoder(fd)
encoder.SetIndent("", " ")
return encoder.Encode(data)
case ".yml", ".yaml":
encoder := yaml.NewEncoder(fd)
return encoder.Encode(data)
default:
return errors.New("invalid format: " + ext)
}
}
func unmarshal(filename string, data interface{}) error {
fd, err := os.Open(filename)
checkErr(err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
case ".json":
return json.NewDecoder(fd).Decode(data)
case ".yml", ".yaml":
return yaml.NewDecoder(fd).Decode(data)
default:
return errors.New("invalid format: " + ext)
}
}
func jsonYamlArg(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
switch ext := filepath.Ext(args[0]); ext {
case ".json", ".yml", ".yaml":
return nil
default:
return errors.New("invalid format: " + ext)
}
}
func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range in {
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
}
return result
}
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
}
}

20
cmd/version.go Normal file
View File

@@ -0,0 +1,20 @@
package cmd
import (
"fmt"
"github.com/filebrowser/filebrowser/v2/version"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("File Browser Version " + version.Version)
},
}

17
errors/errors.go Normal file
View File

@@ -0,0 +1,17 @@
package errors
import "errors"
var (
ErrEmptyKey = errors.New("empty key")
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty")
ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path")
ErrInvalidDataType = errors.New("invalid data type")
ErrIsDirectory = errors.New("file is directory")
ErrInvalidOption = errors.New("invalid option")
ErrInvalidAuthMethod = errors.New("invalid auth method")
)

264
files/file.go Normal file
View File

@@ -0,0 +1,264 @@
package files
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"hash"
"io"
"log"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/spf13/afero"
)
// FileInfo describes a file.
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
}
// FileOptions are the options when getting a file info.
type FileOptions struct {
Fs afero.Fs
Path string
Modify bool
Expand bool
Checker rules.Checker
}
// NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles.
func NewFileInfo(opts FileOptions) (*FileInfo, error) {
if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission
}
info, err := opts.Fs.Stat(opts.Path)
if err != nil {
return nil, err
}
file := &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
}
if opts.Expand {
if file.IsDir {
return file, file.readListing(opts.Checker)
}
err = file.detectType(opts.Modify, true)
if err != nil {
return nil, err
}
}
return file, err
}
// Checksum checksums a given File for a given User, using a specific
// algorithm. The checksums data is saved on File object.
func (i *FileInfo) Checksum(algo string) error {
if i.IsDir {
return errors.ErrIsDirectory
}
if i.Checksums == nil {
i.Checksums = map[string]string{}
}
reader, err := i.Fs.Open(i.Path)
if err != nil {
return err
}
defer reader.Close()
var h hash.Hash
switch algo {
case "md5":
h = md5.New()
case "sha1":
h = sha1.New()
case "sha256":
h = sha256.New()
case "sha512":
h = sha512.New()
default:
return errors.ErrInvalidOption
}
_, err = io.Copy(h, reader)
if err != nil {
return err
}
i.Checksums[algo] = hex.EncodeToString(h.Sum(nil))
return nil
}
func (i *FileInfo) detectType(modify, saveContent bool) error {
// failing to detect the type should not return error.
// imagine the situation where a file in a dir with thousands
// of files couldn't be opened: we'd have immediately
// a 500 even though it doesn't matter. So we just log it.
reader, err := i.Fs.Open(i.Path)
if err != nil {
log.Print(err)
i.Type = "blob"
return nil
}
defer reader.Close()
buffer := make([]byte, 512)
n, err := reader.Read(buffer)
if err != nil && err != io.EOF {
log.Print(err)
i.Type = "blob"
return nil
}
mimetype := mime.TypeByExtension(i.Extension)
if mimetype == "" {
mimetype = http.DetectContentType(buffer[:n])
}
switch {
case strings.HasPrefix(mimetype, "video"):
i.Type = "video"
i.detectSubtitles()
return nil
case strings.HasPrefix(mimetype, "audio"):
i.Type = "audio"
return nil
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
return nil
case isBinary(string(buffer[:n])) || i.Size > 10*1024*1024: // 10 MB
i.Type = "blob"
return nil
default:
i.Type = "text"
if !modify {
i.Type = "textImmutable"
}
if saveContent {
afs := &afero.Afero{Fs: i.Fs}
content, err := afs.ReadFile(i.Path)
if err != nil {
return err
}
i.Content = string(content)
}
}
return nil
}
func (i *FileInfo) detectSubtitles() {
if i.Type != "video" {
return
}
i.Subtitles = []string{}
ext := filepath.Ext(i.Path)
// TODO: detect multiple languages. Base.Lang.vtt
path := strings.TrimSuffix(i.Path, ext) + ".vtt"
if _, err := i.Fs.Stat(path); err == nil {
i.Subtitles = append(i.Subtitles, path)
}
}
func (i *FileInfo) readListing(checker rules.Checker) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
if err != nil {
return err
}
listing := &Listing{
Items: []*FileInfo{},
NumDirs: 0,
NumFiles: 0,
}
for _, f := range dir {
name := f.Name()
path := path.Join(i.Path, name)
if !checker.Check(path) {
continue
}
if strings.HasPrefix(f.Mode().String(), "L") {
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead if the target's.
info, err := i.Fs.Stat(path)
if err == nil {
f = info
}
}
file := &FileInfo{
Fs: i.Fs,
Name: name,
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
Extension: filepath.Ext(name),
Path: path,
}
if file.IsDir {
listing.NumDirs++
} else {
listing.NumFiles++
err := file.detectType(true, false)
if err != nil {
return err
}
}
listing.Items = append(listing.Items, file)
}
i.Listing = listing
return nil
}

108
files/listing.go Normal file
View File

@@ -0,0 +1,108 @@
package files
import (
"sort"
"strings"
"github.com/maruel/natural"
)
// Listing is a collection of files.
type Listing struct {
Items []*FileInfo `json:"items"`
NumDirs int `json:"numDirs"`
NumFiles int `json:"numFiles"`
Sorting Sorting `json:"sorting"`
}
// ApplySort applies the sort order using .Order and .Sort
func (l Listing) ApplySort() {
// Check '.Order' to know how to sort
if !l.Sorting.Asc {
switch l.Sorting.By {
case "name":
sort.Sort(sort.Reverse(byName(l)))
case "size":
sort.Sort(sort.Reverse(bySize(l)))
case "modified":
sort.Sort(sort.Reverse(byModified(l)))
default:
// If not one of the above, do nothing
return
}
} else { // If we had more Orderings we could add them here
switch l.Sorting.By {
case "name":
sort.Sort(byName(l))
case "size":
sort.Sort(bySize(l))
case "modified":
sort.Sort(byModified(l))
default:
sort.Sort(byName(l))
return
}
}
}
// Implement sorting for Listing
type byName Listing
type bySize Listing
type byModified Listing
// By Name
func (l byName) Len() int {
return len(l.Items)
}
func (l byName) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
// Treat upper and lower case equally
func (l byName) Less(i, j int) bool {
if l.Items[i].IsDir && !l.Items[j].IsDir {
return true
}
if !l.Items[i].IsDir && l.Items[j].IsDir {
return false
}
return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name))
}
// By Size
func (l bySize) Len() int {
return len(l.Items)
}
func (l bySize) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
const directoryOffset = -1 << 31 // = math.MinInt32
func (l bySize) Less(i, j int) bool {
iSize, jSize := l.Items[i].Size, l.Items[j].Size
if l.Items[i].IsDir {
iSize = directoryOffset + iSize
}
if l.Items[j].IsDir {
jSize = directoryOffset + jSize
}
return iSize < jSize
}
// By Modified
func (l byModified) Len() int {
return len(l.Items)
}
func (l byModified) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
func (l byModified) Less(i, j int) bool {
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
return iModified.Sub(jModified) < 0
}

7
files/sorting.go Normal file
View File

@@ -0,0 +1,7 @@
package files
// Sorting contains a sorting order.
type Sorting struct {
By string `json:"by"`
Asc bool `json:"asc"`
}

12
files/utils.go Normal file
View File

@@ -0,0 +1,12 @@
package files
func isBinary(content string) bool {
for _, b := range content {
// 65533 is the unknown char
// 8 and below are control chars (e.g. backspace, null, eof, etc)
if b <= 8 || b == 65533 {
return true
}
}
return false
}

39
fileutils/copy.go Normal file
View File

@@ -0,0 +1,39 @@
package fileutils
import (
"os"
"path"
"github.com/spf13/afero"
)
// Copy copies a file or folder from one place to another.
func Copy(fs afero.Fs, src, dst string) error {
if src = path.Clean("/" + src); src == "" {
return os.ErrNotExist
}
if dst = path.Clean("/" + dst); dst == "" {
return os.ErrNotExist
}
if src == "/" || dst == "/" {
// Prohibit copying from or to the virtual root directory.
return os.ErrInvalid
}
if dst == src {
return os.ErrInvalid
}
info, err := fs.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return CopyDir(fs, src, dst)
}
return CopyFile(fs, src, dst)
}

62
fileutils/dir.go Normal file
View File

@@ -0,0 +1,62 @@
package fileutils
import (
"errors"
"github.com/spf13/afero"
)
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source string, dest string) error {
// Get properties of source.
srcinfo, err := fs.Stat(source)
if err != nil {
return err
}
// Create the destination directory.
err = fs.MkdirAll(dest, srcinfo.Mode())
if err != nil {
return err
}
dir, _ := fs.Open(source)
obs, err := dir.Readdir(-1)
if err != nil {
return err
}
var errs []error
for _, obj := range obs {
fsource := source + "/" + obj.Name()
fdest := dest + "/" + obj.Name()
if obj.IsDir() {
// Create sub-directories, recursively.
err = CopyDir(fs, fsource, fdest)
if err != nil {
errs = append(errs, err)
}
} else {
// Perform the file copy.
err = CopyFile(fs, fsource, fdest)
if err != nil {
errs = append(errs, err)
}
}
}
var errString string
for _, err := range errs {
errString += err.Error() + "\n"
}
if errString != "" {
return errors.New(errString)
}
return nil
}

51
fileutils/file.go Normal file
View File

@@ -0,0 +1,51 @@
package fileutils
import (
"io"
"path/filepath"
"github.com/spf13/afero"
)
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(fs afero.Fs, source string, dest string) error {
// Open the source file.
src, err := fs.Open(source)
if err != nil {
return err
}
defer src.Close()
// Makes the directory needed to create the dst
// file.
err = fs.MkdirAll(filepath.Dir(dest), 0666)
if err != nil {
return err
}
// Create the destination file.
dst, err := fs.Create(dest)
if err != nil {
return err
}
defer dst.Close()
// Copy the contents of the file.
_, err = io.Copy(dst, src)
if err != nil {
return err
}
// Copy the mode if the user can't
// open the file.
info, err := fs.Stat(source)
if err != nil {
err = fs.Chmod(dest, info.Mode())
if err != nil {
return err
}
}
return nil
}

31
go.mod
View File

@@ -1,36 +1,45 @@
module github.com/filebrowser/filebrowser
module github.com/filebrowser/filebrowser/v2
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/DataDog/zstd v1.3.4 // indirect
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da
github.com/Sereal/Sereal v0.0.0-20181211220259-509a78ddbda3 // indirect
github.com/Sereal/Sereal v0.0.0-20180905114147-563b78806e28 // indirect
github.com/asdine/storm v2.1.2+incompatible
github.com/boltdb/bolt v1.3.1 // indirect
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gohugoio/hugo v0.49.2
github.com/golang/protobuf v1.2.0 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/google/uuid v1.1.0 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2
github.com/gorilla/websocket v1.4.0
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1
github.com/hacdias/varutils v0.0.0-20171121224303-82d3b57f667a
github.com/hacdias/fileutils v0.0.0-20171121222743-76b1c6ab9067
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1
github.com/mholt/archiver v2.1.0+incompatible
github.com/mholt/archiver v3.1.1+incompatible
github.com/mholt/caddy v0.11.1
github.com/mitchellh/go-homedir v1.0.0
github.com/mitchellh/mapstructure v1.1.2
github.com/nwaples/rardecode v1.0.0 // indirect
github.com/pierrec/lz4 v2.0.5+incompatible // indirect
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/pelletier/go-toml v1.2.0
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1 // indirect
github.com/spf13/afero v1.1.2
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.1
github.com/ulikunitz/xz v0.5.5 // indirect
github.com/vmihailenco/msgpack v4.0.1+incompatible // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go.etcd.io/bbolt v1.3.0 // indirect
go.etcd.io/bbolt v1.3.0
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd // indirect
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
google.golang.org/appengine v1.3.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.2.2
)

116
go.sum
View File

@@ -1,167 +1,123 @@
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
github.com/BurntSushi/toml v0.0.0-20170626110600-a368813c5e64 h1:BuYewlQyh/jroxY8qx41SrzD8Go17GkyCyAeVmprvQI=
github.com/BurntSushi/toml v0.0.0-20170626110600-a368813c5e64/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/zstd v1.3.4 h1:LAGHkXuvC6yky+C2CUG2tD7w8QlrUwpue8XwIh0X4AY=
github.com/DataDog/zstd v1.3.4/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da h1:UVU3a9pRUyLdnBtn60WjRl0s4SEyJc2ChCY56OAR6wI=
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da/go.mod h1:DgrzXonpdQbfN3uYaGz1EG4Sbhyum/MMIn6Cphlh2bw=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Sereal/Sereal v0.0.0-20181211220259-509a78ddbda3/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.5.0 h1:PI0RlRSWL+8GSMuIMMA5KIND4CeJ5KXUQA60LLp/SjA=
github.com/alecthomas/chroma v0.5.0/go.mod h1:MmozekIi2rfQSzDcdEZ2BoJ9Pxs/7uc2Y4Boh+hIeZo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/Sereal/Sereal v0.0.0-20180905114147-563b78806e28 h1:KjLSBawWQq6I0p9VRX8RtHIuttTYvUCGfMgNoBBFxYs=
github.com/Sereal/Sereal v0.0.0-20180905114147-563b78806e28/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
github.com/bep/debounce v1.1.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.0.0/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY=
github.com/bep/go-tocss v0.5.0/go.mod h1:c/+hEVoVvkufrV9Is/CPRHWGGdpcTwNuB48hfxzyYBI=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/chaseadamsio/goorgeous v1.1.0 h1:J9UrYDhzucUMHXsCKG+kICvpR5dT1cqZdVFTYvSlUBk=
github.com/chaseadamsio/goorgeous v1.1.0/go.mod h1:6QaC0vFoKWYDth94dHFNgRT2YkT5FHdQp/Yx15aAAi0=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb h1:tUf55Po0vzOendQ7NWytcdK0VuzQmfAgvGBUOQvN0WA=
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb/go.mod h1:U0vRfAucUOohvdCxt5MWLF+TePIL0xbCkbKIiV8TQCE=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/disintegration/imaging v1.5.0/go.mod h1:9B/deIUIrliYkyMTuXJd6OUFLcrZ2tf+3Qlwnaf/CjU=
github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76 h1:eX+pdPPlD279OWgdx7f6KqIRSONuK7egk+jDx7OM3Ac=
github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76/go.mod h1:KjxHHirfLaw19iGT70HvVjHQsL1vq1SRQB4yOsAfy2s=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gohugoio/hugo v0.49.2 h1:cj62OqvM3tV12G06J9QQkN2GrO0hOq5m0xtREC7Z9NQ=
github.com/gohugoio/hugo v0.49.2/go.mod h1:Mh0VDogJpLC4OWpv/wIE4+tJZ1wFPUfMJDNaoJ1yuFA=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1 h1:2MkEawJQTmAr6YI7T7j7SKxdTmYJOcaJZfzeVPr56PM=
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1/go.mod h1:lwnswzFVSy7B/k81M5rOLUU0fOBKHrDRIkPIBZd7PBo=
github.com/hacdias/varutils v0.0.0-20171121224303-82d3b57f667a h1:BAjqcm8I/EO0WVwhNcZ/Zcv0Gd/7BrhCp2zi8oqXZh4=
github.com/hacdias/varutils v0.0.0-20171121224303-82d3b57f667a/go.mod h1:VfbRoVIe7I1Hz8CEW4K80fCz+lQ6Budq0WiI0MrqEdM=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hacdias/fileutils v0.0.0-20171121222743-76b1c6ab9067 h1:K2ugN3B7NOrATI7GfXRrwtbyg0OYVR9oNcm1XeTIyY4=
github.com/hacdias/fileutils v0.0.0-20171121222743-76b1c6ab9067/go.mod h1:lwnswzFVSy7B/k81M5rOLUU0fOBKHrDRIkPIBZd7PBo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jdkato/prose v1.1.0 h1:LpvmDGwbKGTgdCH3a8VJL56sr7p/wOFPw/R4lM4PfFg=
github.com/jdkato/prose v1.1.0/go.mod h1:jkF0lkxaX5PFSlk9l4Gh9Y+T57TqUZziWT7uZbW5ADg=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v1.5.1 h1:qp9dub1mW7C4MlvoRENH6EAENb9skEFOvIEbp1Waj38=
github.com/kyokomi/emoji v1.5.1/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/magefile/mage v1.4.0/go.mod h1:IUDi13rsHje59lecXokTfGX0QIzO45uVPlXnJYsXepA=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/markbates/inflect v1.0.0/go.mod h1:oTeZL2KHA7CUX6X+fovmK9OvIOFuqu0TwdQrZjLTh88=
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 h1:PEhRT94KBTY4E0KdCYmhvDGWjSFBxc68j2M6PMRix8U=
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1/go.mod h1:wI697HNhDFM/vBruYM3ckbszQ2+DOIeH9qdBKMdf288=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mholt/archiver v2.1.0+incompatible h1:1ivm7KAHPtPere1YDOdrY6xGdbMNGRWThZbYh5lWZT0=
github.com/mholt/archiver v2.1.0+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
github.com/mholt/archiver v3.1.0+incompatible h1:S1rFZ7umHtN6cG+6cusrfoXTMPqp6u/R89iKxBYJd4w=
github.com/mholt/archiver v3.1.0+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
github.com/mholt/caddy v0.11.1 h1:oNfejqftVesLoFxw53Gh17aBPNbTxQ9xJw1pn4IiAPk=
github.com/mholt/caddy v0.11.1/go.mod h1:Wb1PlT4DAYSqOEd03MsqkdkXnTxA8v9pKjdpxbqM1kY=
github.com/miekg/mmark v1.3.6 h1:t47x5vThdwgLJzofNsbsAl7gmIiJ7kbDQN5BxwBmwvY=
github.com/miekg/mmark v1.3.6/go.mod h1:w7r9mkTvpS55jlfyn22qJ618itLryxXBhA7Jp3FIlkw=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/muesli/smartcrop v0.0.0-20180228075044-f6ebaa786a12/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 h1:fiKJgB4JDUd43CApkmCeTSQlWjtTtABrU2qsgbuP0BI=
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1 h1:0utzB5Mn6QyMzIeOn+oD7pjKQLjJwfM9bz6TkPPdxcw=
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba h1:8Vzt8HxRjy7hp1eqPKVoAEPK9npQFW2510qlobGzvi0=
github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sanity-io/litter v1.1.0/go.mod h1:CJ0VCw2q4qKU7LaQr3n7UOSHzgEMgcGco7N/SkZQPjw=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/fsync v0.0.0-20170320142552-12a01e648f05/go.mod h1:jdsEoy1w+v0NpuwXZEaRAH6ADTDmzfRnE2eVwshwFrM=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d/go.mod h1:jU8A+8xL+6n1OX4XaZtCj4B3mIa64tULUsD6YegdpFo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.2.0/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38=
github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tdewolff/minify v2.3.5+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs=
github.com/tdewolff/parse v2.3.3+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ=
github.com/tdewolff/test v0.0.0-20171106182207-265427085153/go.mod h1:DiQUlutnqlEvdvhSn2LPGy4TFwRauAaYDsL+683RNX4=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok=
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/vmihailenco/msgpack v4.0.1+incompatible h1:RMF1enSPeKTlXrXdOcqjFUElywVZjjC6pqse21bKbEU=
github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/wellington/go-libsass v0.0.0-20180624165032-615eaa47ef79/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
go.etcd.io/bbolt v1.3.0 h1:oY10fI923Q5pVCVt1GBTZMn8LHo5M+RCInFpeMnV4QI=
go.etcd.io/bbolt v1.3.0/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

180
http/auth.go Normal file
View File

@@ -0,0 +1,180 @@
package http
import (
"encoding/json"
"net/http"
"os"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users"
)
type userInfo struct {
ID uint `json:"id"`
Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"`
Perm users.Permissions `json:"perm"`
Commands []string `json:"commands"`
LockPassword bool `json:"lockPassword"`
}
type authToken struct {
User userInfo `json:"user"`
jwt.StandardClaims
}
type extractor []string
func (e extractor) ExtractToken(r *http.Request) (string, error) {
token, _ := request.HeaderExtractor{"X-Auth"}.ExtractToken(r)
// Checks if the token isn't empty and if it contains two dots.
// The former prevents incompatibility with URLs that previously
// used basic auth.
if token != "" && strings.Count(token, ".") == 2 {
return token, nil
}
auth := r.URL.Query().Get("auth")
if auth == "" {
return "", request.ErrNoTokenInRequest
}
return auth, nil
}
func withUser(fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
keyFunc := func(token *jwt.Token) (interface{}, error) {
return d.settings.Key, nil
}
var tk authToken
token, err := request.ParseFromRequestWithClaims(r, &extractor{}, &tk, keyFunc)
if err != nil || !token.Valid {
return http.StatusForbidden, nil
}
expired := !tk.VerifyExpiresAt(time.Now().Add(time.Hour).Unix(), true)
updated := d.store.Users.LastUpdate(tk.User.ID) > tk.IssuedAt
if expired || updated {
w.Header().Add("X-Renew-Token", "true")
}
d.user, err = d.store.Users.Get(d.server.Root, tk.User.ID)
if err != nil {
return http.StatusInternalServerError, err
}
return fn(w, r, d)
}
}
func withAdmin(fn handleFunc) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Admin {
return http.StatusForbidden, nil
}
return fn(w, r, d)
})
}
var loginHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
auther, err := d.store.Auth.Get(d.settings.AuthMethod)
if err != nil {
return http.StatusInternalServerError, err
}
user, err := auther.Auth(r, d.store.Users, d.server.Root)
if err == os.ErrPermission {
return http.StatusForbidden, nil
} else if err != nil {
return http.StatusInternalServerError, err
} else {
return printToken(w, r, d, user)
}
}
type signupBody struct {
Username string `json:"username"`
Password string `json:"password"`
}
var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.settings.Signup {
return http.StatusMethodNotAllowed, nil
}
if r.Body == nil {
return http.StatusBadRequest, nil
}
info := &signupBody{}
err := json.NewDecoder(r.Body).Decode(info)
if err != nil {
return http.StatusBadRequest, err
}
if info.Password == "" || info.Username == "" {
return http.StatusBadRequest, nil
}
user := &users.User{
Username: info.Username,
}
d.settings.Defaults.Apply(user)
pwd, err := users.HashPwd(info.Password)
if err != nil {
return http.StatusInternalServerError, err
}
user.Password = pwd
err = d.store.Users.Save(user)
if err == errors.ErrExist {
return http.StatusConflict, err
} else if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
return printToken(w, r, d, d.user)
})
func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.User) (int, error) {
claims := &authToken{
User: userInfo{
ID: user.ID,
Locale: user.Locale,
ViewMode: user.ViewMode,
Perm: user.Perm,
LockPassword: user.LockPassword,
Commands: user.Commands,
},
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(time.Hour * 2).Unix(),
Issuer: "File Browser",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(d.settings.Key)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "cty")
w.Write([]byte(signed))
return 0, nil
}

104
http/commands.go Normal file
View File

@@ -0,0 +1,104 @@
package http
import (
"bufio"
"io"
"log"
"net/http"
"os/exec"
"strings"
"time"
"github.com/filebrowser/filebrowser/v2/runner"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
var (
cmdNotAllowed = []byte("Command not allowed.")
)
func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) {
txt := http.StatusText(status)
if err != nil || status >= 400 {
log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err)
}
ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(10*time.Second))
}
var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return http.StatusInternalServerError, err
}
defer conn.Close()
var raw string
for {
_, msg, err := conn.ReadMessage()
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
return 0, nil
}
raw = strings.TrimSpace(string(msg))
if raw != "" {
break
}
}
if !d.user.CanExecute(strings.Split(raw, " ")[0]) {
err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed)
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
}
return 0, nil
}
command, err := runner.ParseCommand(d.settings, raw)
if err != nil {
err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
}
return 0, nil
}
cmd := exec.Command(command[0], command[1:]...)
cmd.Dir = d.user.FullPath(r.URL.Path)
stdout, err := cmd.StdoutPipe()
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
return 0, nil
}
stderr, err := cmd.StderrPipe()
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
return 0, nil
}
if err := cmd.Start(); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
return 0, nil
}
s := bufio.NewScanner(io.MultiReader(stdout, stderr))
for s.Scan() {
conn.WriteMessage(websocket.TextMessage, s.Bytes())
}
if err := cmd.Wait(); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
}
return 0, nil
})

68
http/data.go Normal file
View File

@@ -0,0 +1,68 @@
package http
import (
"log"
"net/http"
"strconv"
"github.com/filebrowser/filebrowser/v2/runner"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
)
type handleFunc func(w http.ResponseWriter, r *http.Request, d *data) (int, error)
type data struct {
*runner.Runner
settings *settings.Settings
server *settings.Server
store *storage.Storage
user *users.User
raw interface{}
}
// Check implements rules.Checker.
func (d *data) Check(path string) bool {
for _, rule := range d.user.Rules {
if rule.Matches(path) {
return rule.Allow
}
}
for _, rule := range d.settings.Rules {
if rule.Matches(path) {
return rule.Allow
}
}
return true
}
func handle(fn handleFunc, prefix string, storage *storage.Storage, server *settings.Server) http.Handler {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
settings, err := storage.Settings.Get()
if err != nil {
log.Fatalln("ERROR: couldn't get settings")
return
}
status, err := fn(w, r, &data{
Runner: &runner.Runner{Settings: settings},
store: storage,
settings: settings,
server: server,
})
if status != 0 {
txt := http.StatusText(status)
http.Error(w, strconv.Itoa(status)+" "+txt, status)
}
if status >= 400 || err != nil {
log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err)
}
})
return http.StripPrefix(prefix, handler)
}

64
http/http.go Normal file
View File

@@ -0,0 +1,64 @@
package http
import (
"net/http"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/gorilla/mux"
)
type modifyRequest struct {
What string `json:"what"` // Answer to: what data type?
Which []string `json:"which"` // Answer to: which fields?
}
func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler, error) {
server.Clean()
r := mux.NewRouter()
index, static := getStaticHandlers(storage, server)
monkey := func(fn handleFunc, prefix string) http.Handler {
return handle(fn, prefix, storage, server)
}
r.PathPrefix("/static").Handler(static)
r.NotFoundHandler = index
api := r.PathPrefix("/api").Subrouter()
api.Handle("/login", monkey(loginHandler, ""))
api.Handle("/signup", monkey(signupHandler, ""))
api.Handle("/renew", monkey(renewHandler, ""))
users := api.PathPrefix("/users").Subrouter()
users.Handle("", monkey(usersGetHandler, "")).Methods("GET")
users.Handle("", monkey(userPostHandler, "")).Methods("POST")
users.Handle("/{id:[0-9]+}", monkey(userPutHandler, "")).Methods("PUT")
users.Handle("/{id:[0-9]+}", monkey(userGetHandler, "")).Methods("GET")
users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE")
api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET")
api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler, "/api/resources")).Methods("DELETE")
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("POST")
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("PUT")
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler, "/api/resources")).Methods("PATCH")
api.PathPrefix("/share").Handler(monkey(shareGetsHandler, "/api/share")).Methods("GET")
api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST")
api.PathPrefix("/share").Handler(monkey(shareDeleteHandler, "/api/share")).Methods("DELETE")
api.Handle("/settings", monkey(settingsGetHandler, "")).Methods("GET")
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
public := api.PathPrefix("/public").Subrouter()
public.PathPrefix("/dl").Handler(monkey(publicDlHandler, "/api/public/dl/")).Methods("GET")
public.PathPrefix("/share").Handler(monkey(publicShareHandler, "/api/public/share/")).Methods("GET")
return http.StripPrefix(server.BaseURL, r), nil
}

50
http/public.go Normal file
View File

@@ -0,0 +1,50 @@
package http
import (
"net/http"
"github.com/filebrowser/filebrowser/v2/files"
)
var withHashFile = func(fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
link, err := d.store.Share.GetByHash(r.URL.Path)
if err != nil {
return errToStatus(err), err
}
user, err := d.store.Users.Get(d.server.Root, link.UserID)
if err != nil {
return errToStatus(err), err
}
d.user = user
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: link.Path,
Modify: d.user.Perm.Modify,
Expand: false,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
d.raw = file
return fn(w, r, d)
}
}
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
return renderJSON(w, r, d.raw)
})
var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
file := d.raw.(*files.FileInfo)
if !file.IsDir {
return rawFileHandler(w, r, file)
}
return rawDirHandler(w, r, d, file)
})

179
http/raw.go Normal file
View File

@@ -0,0 +1,179 @@
package http
import (
"errors"
"fmt"
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
)
func parseQueryFiles(r *http.Request, f *files.FileInfo, u *users.User) ([]string, error) {
files := []string{}
names := strings.Split(r.URL.Query().Get("files"), ",")
if len(names) == 0 {
files = append(files, f.Path)
} else {
for _, name := range names {
name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1))
if err != nil {
return nil, err
}
name = fileutils.SlashClean(name)
files = append(files, filepath.Join(f.Path, name))
}
}
return files, nil
}
func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
switch r.URL.Query().Get("algo") {
case "zip", "true", "":
return ".zip", archiver.NewZip(), nil
case "tar":
return ".tar", archiver.NewTar(), nil
case "targz":
return ".tar.gz", archiver.NewTarGz(), nil
case "tarbz2":
return ".tar.bz2", archiver.NewTarBz2(), nil
case "tarxz":
return ".tar.xz", archiver.NewTarXz(), nil
case "tarlz4":
return ".tar.lz4", archiver.NewTarLz4(), nil
case "tarsz":
return ".tar.sz", archiver.NewTarSz(), nil
default:
return "", nil, errors.New("format not implemented")
}
}
var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Download {
return http.StatusAccepted, nil
}
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: false,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
if !file.IsDir {
return rawFileHandler(w, r, file)
}
return rawDirHandler(w, r, d, file)
})
func addFile(ar archiver.Writer, d *data, path string) error {
// Checks are always done with paths with "/" as path separator.
path = strings.Replace(path, "\\", "/", -1)
fmt.Println(path)
if !d.Check(path) {
return nil
}
info, err := d.user.Fs.Stat(path)
if err != nil {
return err
}
file, err := d.user.Fs.Open(path)
if err != nil {
return err
}
defer file.Close()
err = ar.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: info,
CustomName: strings.TrimPrefix(path, "/"),
},
ReadCloser: file,
})
if err != nil {
return err
}
if info.IsDir() {
names, err := file.Readdirnames(0)
if err != nil {
return err
}
for _, name := range names {
err = addFile(ar, d, filepath.Join(path, name))
if err != nil {
return err
}
}
}
return nil
}
func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.FileInfo) (int, error) {
filenames, err := parseQueryFiles(r, file, d.user)
if err != nil {
return http.StatusInternalServerError, err
}
extension, ar, err := parseQueryAlgorithm(r)
if err != nil {
return http.StatusInternalServerError, err
}
name := file.Name
if name == "." || name == "" {
name = "archive"
}
name += extension
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
err = ar.Create(w)
if err != nil {
return http.StatusInternalServerError, err
}
defer ar.Close()
for _, fname := range filenames {
err = addFile(ar, d, fname)
if err != nil {
return http.StatusInternalServerError, err
}
}
return 0, nil
}
func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo) (int, error) {
fd, err := file.Fs.Open(file.Path)
if err != nil {
return http.StatusInternalServerError, err
}
defer fd.Close()
if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline")
} else {
// As per RFC6266 section 4.3
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
}
http.ServeContent(w, r, file.Name, file.ModTime, fd)
return 0, nil
}

158
http/resource.go Normal file
View File

@@ -0,0 +1,158 @@
package http
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/fileutils"
)
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
if file.IsDir {
file.Listing.Sorting = d.user.Sorting
file.Listing.ApplySort()
return renderJSON(w, r, file)
}
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
err := file.Checksum(checksum)
if err == errors.ErrInvalidOption {
return http.StatusBadRequest, nil
} else if err != nil {
return http.StatusInternalServerError, err
}
// do not waste bandwidth if we just want the checksum
file.Content = ""
}
return renderJSON(w, r, file)
})
var resourceDeleteHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if r.URL.Path == "/" || !d.user.Perm.Delete {
return http.StatusForbidden, nil
}
err := d.RunHook(func() error {
return d.user.Fs.RemoveAll(r.URL.Path)
}, "delete", r.URL.Path, "", d.user)
if err != nil {
return errToStatus(err), err
}
return http.StatusOK, nil
})
var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Create && r.Method == http.MethodPost {
return http.StatusForbidden, nil
}
if !d.user.Perm.Modify && r.Method == http.MethodPut {
return http.StatusForbidden, nil
}
defer func() {
io.Copy(ioutil.Discard, r.Body)
}()
// For directories, only allow POST for creation.
if strings.HasSuffix(r.URL.Path, "/") {
if r.Method == http.MethodPut {
return http.StatusMethodNotAllowed, nil
}
err := d.user.Fs.MkdirAll(r.URL.Path, 0775)
return errToStatus(err), err
}
if r.Method == http.MethodPost && r.URL.Query().Get("override") != "true" {
if _, err := d.user.Fs.Stat(r.URL.Path); err == nil {
return http.StatusConflict, nil
}
}
err := d.RunHook(func() error {
file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, r.Body)
if err != nil {
return err
}
// Gets the info about the file.
info, err := file.Stat()
if err != nil {
return err
}
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
return nil
}, "upload", r.URL.Path, "", d.user)
return errToStatus(err), err
})
var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
src := r.URL.Path
dst := r.URL.Query().Get("destination")
action := r.URL.Query().Get("action")
dst, err := url.QueryUnescape(dst)
if err != nil {
return errToStatus(err), err
}
if dst == "/" || src == "/" {
return http.StatusForbidden, nil
}
switch action {
case "copy":
if !d.user.Perm.Create {
return http.StatusForbidden, nil
}
case "rename":
default:
action = "rename"
if !d.user.Perm.Rename {
return http.StatusForbidden, nil
}
}
err = d.RunHook(func() error {
if action == "copy" {
return fileutils.Copy(d.user.Fs, src, dst)
}
return d.user.Fs.Rename(src, dst)
}, action, src, dst, d.user)
return errToStatus(err), err
})

28
http/search.go Normal file
View File

@@ -0,0 +1,28 @@
package http
import (
"net/http"
"os"
"github.com/filebrowser/filebrowser/v2/search"
)
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
response := []map[string]interface{}{}
query := r.URL.Query().Get("query")
err := search.Search(d.user.Fs, r.URL.Path, query, d, func(path string, f os.FileInfo) error {
response = append(response, map[string]interface{}{
"dir": f.IsDir(),
"path": path,
})
return nil
})
if err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, r, response)
})

49
http/settings.go Normal file
View File

@@ -0,0 +1,49 @@
package http
import (
"encoding/json"
"net/http"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/settings"
)
type settingsData struct {
Signup bool `json:"signup"`
Defaults settings.UserDefaults `json:"defaults"`
Rules []rules.Rule `json:"rules"`
Branding settings.Branding `json:"branding"`
Shell []string `json:"shell"`
Commands map[string][]string `json:"commands"`
}
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
data := &settingsData{
Signup: d.settings.Signup,
Defaults: d.settings.Defaults,
Rules: d.settings.Rules,
Branding: d.settings.Branding,
Shell: d.settings.Shell,
Commands: d.settings.Commands,
}
return renderJSON(w, r, data)
})
var settingsPutHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
req := &settingsData{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
return http.StatusBadRequest, err
}
d.settings.Signup = req.Signup
d.settings.Defaults = req.Defaults
d.settings.Rules = req.Rules
d.settings.Branding = req.Branding
d.settings.Shell = req.Shell
d.settings.Commands = req.Commands
err = d.store.Settings.Save(d.settings)
return errToStatus(err), err
})

107
http/share.go Normal file
View File

@@ -0,0 +1,107 @@
package http
import (
"crypto/rand"
"encoding/base64"
"net/http"
"strconv"
"strings"
"time"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/share"
)
func withPermShare(fn handleFunc) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Share {
return http.StatusForbidden, nil
}
return fn(w, r, d)
})
}
var shareGetsHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
s, err := d.store.Share.Gets(r.URL.Path, d.user.ID)
if err == errors.ErrNotExist {
return renderJSON(w, r, []*share.Link{})
}
if err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, r, s)
})
var shareDeleteHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
hash := strings.TrimSuffix(r.URL.Path, "/")
hash = strings.TrimPrefix(hash, "/")
if hash == "" {
return http.StatusBadRequest, nil
}
err := d.store.Share.Delete(hash)
return errToStatus(err), err
})
var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
var s *share.Link
rawExpire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit")
if rawExpire == "" {
var err error
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
if err == nil {
w.Write([]byte(d.server.BaseURL + "/share/" + s.Hash))
return 0, nil
}
}
bytes := make([]byte, 6)
_, err := rand.Read(bytes)
if err != nil {
return http.StatusInternalServerError, err
}
str := base64.URLEncoding.EncodeToString(bytes)
var expire int64 = 0
if rawExpire != "" {
num, err := strconv.Atoi(rawExpire)
if err != nil {
return http.StatusInternalServerError, err
}
var add time.Duration
switch unit {
case "seconds":
add = time.Second * time.Duration(num)
case "minutes":
add = time.Minute * time.Duration(num)
case "days":
add = time.Hour * 24 * time.Duration(num)
default:
add = time.Hour * time.Duration(num)
}
expire = time.Now().Add(add).Unix()
}
s = &share.Link{
Path: r.URL.Path,
Hash: str,
Expire: expire,
UserID: d.user.ID,
}
if err := d.store.Share.Save(s); err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, r, s)
})

126
http/static.go Normal file
View File

@@ -0,0 +1,126 @@
package http
import (
"encoding/json"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
rice "github.com/GeertJohan/go.rice"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/version"
)
func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *rice.Box, file, contentType string) (int, error) {
w.Header().Set("Content-Type", contentType)
staticURL := strings.TrimPrefix(d.server.BaseURL+"/static", "/")
auther, err := d.store.Auth.Get(d.settings.AuthMethod)
if err != nil {
return http.StatusInternalServerError, err
}
data := map[string]interface{}{
"Name": d.settings.Branding.Name,
"DisableExternal": d.settings.Branding.DisableExternal,
"BaseURL": d.server.BaseURL,
"Version": version.Version,
"StaticURL": staticURL,
"Signup": d.settings.Signup,
"NoAuth": d.settings.AuthMethod == auth.MethodNoAuth,
"LoginPage": auther.LoginPage(),
"CSS": false,
"ReCaptcha": false,
}
if d.settings.Branding.Files != "" {
path := filepath.Join(d.settings.Branding.Files, "custom.css")
_, err := os.Stat(path)
if err != nil && !os.IsNotExist(err) {
log.Printf("couldn't load custom styles: %v", err)
}
if err == nil {
data["CSS"] = true
}
}
if d.settings.AuthMethod == auth.MethodJSONAuth {
raw, err := d.store.Auth.Get(d.settings.AuthMethod)
if err != nil {
return http.StatusInternalServerError, err
}
auther := raw.(*auth.JSONAuth)
if auther.ReCaptcha != nil {
data["ReCaptcha"] = auther.ReCaptcha.Key != "" && auther.ReCaptcha.Secret != ""
data["ReCaptchaHost"] = auther.ReCaptcha.Host
data["ReCaptchaKey"] = auther.ReCaptcha.Key
}
}
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return http.StatusInternalServerError, err
}
data["Json"] = string(b)
index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(box.MustString(file)))
err = index.Execute(w, data)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
func getStaticHandlers(storage *storage.Storage, server *settings.Server) (http.Handler, http.Handler) {
box := rice.MustFindBox("../frontend/dist")
handler := http.FileServer(box.HTTPBox())
index := handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if r.Method != http.MethodGet {
return http.StatusNotFound, nil
}
w.Header().Set("x-xss-protection", "1; mode=block")
return handleWithStaticData(w, r, d, box, "index.html", "text/html; charset=utf-8")
}, "", storage, server)
static := handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if r.Method != http.MethodGet {
return http.StatusNotFound, nil
}
if d.settings.Branding.Files != "" {
if strings.HasPrefix(r.URL.Path, "img/") {
path := filepath.Join(d.settings.Branding.Files, r.URL.Path)
if _, err := os.Stat(path); err == nil {
http.ServeFile(w, r, path)
return 0, nil
}
} else if r.URL.Path == "custom.css" && d.settings.Branding.Files != "" {
http.ServeFile(w, r, filepath.Join(d.settings.Branding.Files, "custom.css"))
return 0, nil
}
}
if !strings.HasSuffix(r.URL.Path, ".js") {
handler.ServeHTTP(w, r)
return 0, nil
}
return handleWithStaticData(w, r, d, box, r.URL.Path, "application/javascript; charset=utf-8")
}, "/static/", storage, server)
return index, static
}

186
http/users.go Normal file
View File

@@ -0,0 +1,186 @@
package http
import (
"encoding/json"
"net/http"
"sort"
"strconv"
"strings"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/gorilla/mux"
)
type modifyUserRequest struct {
modifyRequest
Data *users.User `json:"data"`
}
func getUserID(r *http.Request) (uint, error) {
vars := mux.Vars(r)
i, err := strconv.ParseUint(vars["id"], 10, 0)
if err != nil {
return 0, err
}
return uint(i), err
}
func getUser(w http.ResponseWriter, r *http.Request) (*modifyUserRequest, error) {
if r.Body == nil {
return nil, errors.ErrEmptyRequest
}
req := &modifyUserRequest{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
return nil, err
}
if req.What != "user" {
return nil, errors.ErrInvalidDataType
}
return req, nil
}
func withSelfOrAdmin(fn handleFunc) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
id, err := getUserID(r)
if err != nil {
return http.StatusInternalServerError, err
}
if d.user.ID != id && !d.user.Perm.Admin {
return http.StatusForbidden, nil
}
d.raw = id
return fn(w, r, d)
})
}
var usersGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
users, err := d.store.Users.Gets(d.server.Root)
if err != nil {
return http.StatusInternalServerError, err
}
for _, u := range users {
u.Password = ""
}
sort.Slice(users, func(i, j int) bool {
return users[i].ID < users[j].ID
})
return renderJSON(w, r, users)
})
var userGetHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
u, err := d.store.Users.Get(d.server.Root, d.raw.(uint))
if err == errors.ErrNotExist {
return http.StatusNotFound, err
}
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = ""
return renderJSON(w, r, u)
})
var userDeleteHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
err := d.store.Users.Delete(d.raw.(uint))
if err == errors.ErrNotExist {
return http.StatusNotFound, err
}
return http.StatusOK, nil
})
var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
req, err := getUser(w, r)
if err != nil {
return http.StatusBadRequest, err
}
if len(req.Which) != 0 {
return http.StatusBadRequest, nil
}
if req.Data.Password == "" {
return http.StatusBadRequest, errors.ErrEmptyPassword
}
req.Data.Password, err = users.HashPwd(req.Data.Password)
if err != nil {
return http.StatusInternalServerError, err
}
err = d.store.Users.Save(req.Data)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Location", "/settings/users/"+strconv.FormatUint(uint64(req.Data.ID), 10))
return http.StatusCreated, nil
})
var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
req, err := getUser(w, r)
if err != nil {
return http.StatusBadRequest, err
}
if req.Data.ID != d.raw.(uint) {
return http.StatusBadRequest, nil
}
if len(req.Which) == 1 && req.Which[0] == "all" {
if !d.user.Perm.Admin {
return http.StatusForbidden, err
}
if req.Data.Password != "" {
req.Data.Password, err = users.HashPwd(req.Data.Password)
} else {
var suser *users.User
suser, err = d.store.Users.Get(d.server.Root, d.raw.(uint))
req.Data.Password = suser.Password
}
if err != nil {
return http.StatusInternalServerError, err
}
req.Which = []string{}
}
for k, v := range req.Which {
if v == "password" {
if !d.user.Perm.Admin && d.user.LockPassword {
return http.StatusForbidden, nil
}
req.Data.Password, err = users.HashPwd(req.Data.Password)
if err != nil {
return http.StatusInternalServerError, err
}
}
if !d.user.Perm.Admin && (v == "scope" || v == "perm" || v == "username") {
return http.StatusForbidden, nil
}
req.Which[k] = strings.Title(v)
}
err = d.store.Users.Update(req.Data, req.Which...)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
})

39
http/utils.go Normal file
View File

@@ -0,0 +1,39 @@
package http
import (
"encoding/json"
"net/http"
"os"
"github.com/filebrowser/filebrowser/v2/errors"
)
func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) (int, error) {
marsh, err := json.Marshal(data)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write(marsh); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
func errToStatus(err error) int {
switch {
case err == nil:
return http.StatusOK
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err), err == errors.ErrNotExist:
return http.StatusNotFound
case os.IsExist(err), err == errors.ErrExist:
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}

View File

@@ -1,26 +0,0 @@
package bolt
import (
"github.com/asdine/storm"
fb "github.com/filebrowser/filebrowser/lib"
)
// ConfigStore is a configuration store.
type ConfigStore struct {
DB *storm.DB
}
// Get gets a configuration from the database to an interface.
func (c ConfigStore) Get(name string, to interface{}) error {
err := c.DB.Get("config", name, to)
if err == storm.ErrNotFound {
return fb.ErrNotExist
}
return err
}
// Save saves a configuration from an interface to the database.
func (c ConfigStore) Save(name string, from interface{}) error {
return c.DB.Set("config", name, from)
}

View File

@@ -1,66 +0,0 @@
package bolt
import (
"github.com/asdine/storm"
"github.com/asdine/storm/q"
fb "github.com/filebrowser/filebrowser/lib"
)
// ShareStore is a shareable links store.
type ShareStore struct {
DB *storm.DB
}
// Get gets a Share Link from an hash.
func (s ShareStore) Get(hash string) (*fb.ShareLink, error) {
var v fb.ShareLink
err := s.DB.One("Hash", hash, &v)
if err == storm.ErrNotFound {
return nil, fb.ErrNotExist
}
return &v, err
}
// GetPermanent gets the permanent link from a path.
func (s ShareStore) GetPermanent(path string) (*fb.ShareLink, error) {
var v fb.ShareLink
err := s.DB.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&v)
if err == storm.ErrNotFound {
return nil, fb.ErrNotExist
}
return &v, err
}
// GetByPath gets all the links for a specific path.
func (s ShareStore) GetByPath(hash string) ([]*fb.ShareLink, error) {
var v []*fb.ShareLink
err := s.DB.Find("Path", hash, &v)
if err == storm.ErrNotFound {
return v, fb.ErrNotExist
}
return v, err
}
// Gets retrieves all the shareable links.
func (s ShareStore) Gets() ([]*fb.ShareLink, error) {
var v []*fb.ShareLink
err := s.DB.All(&v)
if err == storm.ErrNotFound {
return v, fb.ErrNotExist
}
return v, err
}
// Save stores a Share Link on the database.
func (s ShareStore) Save(l *fb.ShareLink) error {
return s.DB.Save(l)
}
// Delete deletes a Share Link from the database.
func (s ShareStore) Delete(hash string) error {
return s.DB.DeleteStruct(&fb.ShareLink{Hash: hash})
}

View File

@@ -1,90 +0,0 @@
package bolt
import (
"reflect"
"github.com/asdine/storm"
fb "github.com/filebrowser/filebrowser/lib"
)
// UsersStore is a users store.
type UsersStore struct {
DB *storm.DB
}
// Get gets a user with a certain id from the database.
func (u UsersStore) Get(id int, builder fb.FSBuilder) (*fb.User, error) {
var us fb.User
err := u.DB.One("ID", id, &us)
if err == storm.ErrNotFound {
return nil, fb.ErrNotExist
}
if err != nil {
return nil, err
}
us.FileSystem = builder(us.Scope)
return &us, nil
}
// GetByUsername gets a user with a certain username from the database.
func (u UsersStore) GetByUsername(username string, builder fb.FSBuilder) (*fb.User, error) {
var us fb.User
err := u.DB.One("Username", username, &us)
if err == storm.ErrNotFound {
return nil, fb.ErrNotExist
}
if err != nil {
return nil, err
}
us.FileSystem = builder(us.Scope)
return &us, nil
}
// Gets gets all the users from the database.
func (u UsersStore) Gets(builder fb.FSBuilder) ([]*fb.User, error) {
var us []*fb.User
err := u.DB.All(&us)
if err == storm.ErrNotFound {
return nil, fb.ErrNotExist
}
if err != nil {
return us, err
}
for _, user := range us {
user.FileSystem = builder(user.Scope)
}
return us, err
}
// Update updates the whole user object or only certain fields.
func (u UsersStore) Update(us *fb.User, fields ...string) error {
if len(fields) == 0 {
return u.Save(us)
}
for _, field := range fields {
val := reflect.ValueOf(us).Elem().FieldByName(field).Interface()
if err := u.DB.UpdateField(us, field, val); err != nil {
return err
}
}
return nil
}
// Save saves a user to the database.
func (u UsersStore) Save(us *fb.User) error {
return u.DB.Save(us)
}
// Delete deletes a user from the database.
func (u UsersStore) Delete(id int) error {
return u.DB.DeleteStruct(&fb.User{ID: id})
}

View File

@@ -1,77 +0,0 @@
/*
Package filebrowser provides a web interface to access your files
wherever you are. To use this package as a middleware for your app,
you'll need to import both File Browser and File Browser HTTP packages.
import (
fm "github.com/filebrowser/filebrowser"
h "github.com/filebrowser/filebrowser/http"
)
Then, you should create a new FileBrowser object with your options. In this
case, I'm using BoltDB (via Storm package) as a Store. So, you'll also need
to import "github.com/filebrowser/filebrowser/bolt".
db, _ := storm.Open("bolt.db")
m := &fm.FileBrowser{
NoAuth: false,
Auth: {
Method: "default",
LoginHeader: "X-Fowarded-User"
},
DefaultUser: &fm.User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{"git"},
Rules: []*fm.Rule{},
Locale: "en",
CSS: "",
Scope: ".",
FileSystem: fileutils.Dir("."),
},
Store: &fm.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) fm.FileSystem {
return fileutils.Dir(scope)
},
}
The credentials for the first user are always 'admin' for both the user and
the password, and they can be changed later through the settings. The first
user is always an Admin and has all of the permissions set to 'true'.
Then, you should set the Prefix URL and the Base URL, using the following
functions:
m.SetBaseURL("/")
m.SetPrefixURL("/")
The Prefix URL is a part of the path that is already stripped from the
r.URL.Path variable before the request arrives to File Browser's handler.
This is a function that will rarely be used. You can see one example on Caddy
filemanager plugin.
The Base URL is the URL path where you want File Browser to be available in. If
you want to be available at the root path, you should call:
m.SetBaseURL("/")
But if you want to access it at '/admin', you would call:
m.SetBaseURL("/admin")
Now, that you already have a File Browser instance created, you just need to
add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
One simple implementation for this, at port 80, in the root of the domain, would be:
http.ListenAndServe(":80", h.Handler(m))
*/
package lib

View File

@@ -1,491 +0,0 @@
package lib
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"hash"
"io"
"io/ioutil"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/gohugoio/hugo/parser"
"github.com/maruel/natural"
)
// The size of the loaded text can be rendered in the browser. Avoiding files that are too large causes browsers to crash.
// Currently set to 10MB, 10 * 1024 * 1024 = 10485760 byte
const textExtensionsRenderMaxSize int64 = 10485760
// File contains the information about a particular file or directory.
type File struct {
// Indicates the Kind of view on the front-end (Listing, editor or preview).
Kind string `json:"kind"`
// The name of the file.
Name string `json:"name"`
// The Size of the file.
Size int64 `json:"size"`
// The absolute URL.
URL string `json:"url"`
// The extension of the file.
Extension string `json:"extension"`
// The last modified time.
ModTime time.Time `json:"modified"`
// The File Mode.
Mode os.FileMode `json:"mode"`
// Indicates if this file is a directory.
IsDir bool `json:"isDir"`
// Absolute path.
Path string `json:"path"`
// Relative path to user's virtual File System.
VirtualPath string `json:"virtualPath"`
// Indicates the file content type: video, text, image, music or blob.
Type string `json:"type"`
// Stores the content of a text file.
Content string `json:"content,omitempty"`
*Listing `json:",omitempty"`
Metadata string `json:"metadata,omitempty"`
Language string `json:"language,omitempty"`
}
// A Listing is the context used to fill out a template.
type Listing struct {
// The items (files and folders) in the path.
Items []*File `json:"items"`
// The number of directories in the Listing.
NumDirs int `json:"numDirs"`
// The number of files (items that aren't directories) in the Listing.
NumFiles int `json:"numFiles"`
// Which sorting order is used.
Sort string `json:"sort"`
// And which order.
Order string `json:"order"`
}
// GetInfo gets the file information and, in case of error, returns the
// respective HTTP error code
func GetInfo(url *url.URL, c *FileBrowser, u *User) (*File, error) {
var err error
i := &File{
URL: "/files" + url.String(),
VirtualPath: url.Path,
Path: filepath.Join(u.Scope, url.Path),
}
info, err := u.FileSystem.Stat(url.Path)
if err != nil {
return i, err
}
i.Name = info.Name()
i.ModTime = info.ModTime()
i.Mode = info.Mode()
i.IsDir = info.IsDir()
i.Size = info.Size()
i.Extension = filepath.Ext(i.Name)
if i.IsDir && !strings.HasSuffix(i.URL, "/") {
i.URL += "/"
}
return i, nil
}
// GetListing gets the information about a specific directory and its files.
func (i *File) GetListing(u *User, r *http.Request) error {
// Gets the directory information using the Virtual File System of
// the user configuration.
f, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
if err != nil {
return err
}
defer f.Close()
// Reads the directory and gets the information about the files.
files, err := f.Readdir(-1)
if err != nil {
return err
}
var (
fileinfos []*File
dirCount, fileCount int
)
baseurl, err := url.PathUnescape(i.URL)
if err != nil {
return err
}
for _, f := range files {
name := f.Name()
allowed := u.Allowed("/" + name)
if !allowed {
continue
}
if strings.HasPrefix(f.Mode().String(), "L") {
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead if the target's.
info, err := os.Stat(f.Name())
if err == nil {
f = info
}
}
if f.IsDir() {
name += "/"
dirCount++
} else {
fileCount++
}
// Absolute URL
url := url.URL{Path: baseurl + name}
i := &File{
Name: f.Name(),
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
URL: url.String(),
Extension: filepath.Ext(name),
VirtualPath: filepath.Join(i.VirtualPath, name),
Path: filepath.Join(i.Path, name),
}
i.GetFileType(false)
fileinfos = append(fileinfos, i)
}
i.Listing = &Listing{
Items: fileinfos,
NumDirs: dirCount,
NumFiles: fileCount,
}
return nil
}
// GetEditor gets the editor based on a Info struct
func (i *File) GetEditor() error {
i.Language = editorLanguage(i.Extension)
// If the editor will hold only content, leave now.
if editorMode(i.Language) == "content" {
return nil
}
// If the file doesn't have any kind of metadata, leave now.
if !hasRune(i.Content) {
return nil
}
buffer := bytes.NewBuffer([]byte(i.Content))
page, err := parser.ReadFrom(buffer)
// If there is an error, just ignore it and return nil.
// This way, the file can be served for editing.
if err != nil {
return nil
}
i.Content = strings.TrimSpace(string(page.Content()))
i.Metadata = strings.TrimSpace(string(page.FrontMatter()))
return nil
}
// GetFileType obtains the mimetype and converts it to a simple
// type nomenclature.
func (i *File) GetFileType(checkContent bool) error {
var content []byte
var err error
// Tries to get the file mimetype using its extension.
mimetype := mime.TypeByExtension(i.Extension)
if mimetype == "" && checkContent {
file, err := os.Open(i.Path)
if err != nil {
return err
}
defer file.Close()
// Only the first 512 bytes are used to sniff the content type.
buffer := make([]byte, 512)
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
return err
}
// Tries to get the file mimetype using its first
// 512 bytes.
mimetype = http.DetectContentType(buffer[:n])
}
if strings.HasPrefix(mimetype, "video") {
i.Type = "video"
return nil
}
if strings.HasPrefix(mimetype, "audio") {
i.Type = "audio"
return nil
}
if strings.HasPrefix(mimetype, "image") {
i.Type = "image"
return nil
}
if strings.HasPrefix(mimetype, "text") {
i.Type = "text"
goto End
}
if strings.HasPrefix(mimetype, "application/javascript") {
i.Type = "text"
goto End
}
// If the type isn't text (and is blob for example), it will check some
// common types that are mistaken not to be text.
if isInTextExtensions(i.Name) {
i.Type = "text"
} else {
i.Type = "blob"
}
End:
// If the file type is text, save its content.
if i.Type == "text" && i.Size <= textExtensionsRenderMaxSize { // Avoiding files that are too large causes browsers to crash
if len(content) == 0 {
content, err = ioutil.ReadFile(i.Path)
if err != nil {
return err
}
}
i.Content = string(content)
}
return nil
}
// Checksum retrieves the checksum of a file.
func (i File) Checksum(algo string) (string, error) {
file, err := os.Open(i.Path)
if err != nil {
return "", err
}
defer file.Close()
var h hash.Hash
switch algo {
case "md5":
h = md5.New()
case "sha1":
h = sha1.New()
case "sha256":
h = sha256.New()
case "sha512":
h = sha512.New()
default:
return "", ErrInvalidOption
}
_, err = io.Copy(h, file)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// CanBeEdited checks if the extension of a file is supported by the editor
func (i File) CanBeEdited() bool {
return i.Type == "text"
}
// ApplySort applies the sort order using .Order and .Sort
func (l Listing) ApplySort() {
// Check '.Order' to know how to sort
if l.Order == "desc" {
switch l.Sort {
case "name":
sort.Sort(sort.Reverse(byName(l)))
case "size":
sort.Sort(sort.Reverse(bySize(l)))
case "modified":
sort.Sort(sort.Reverse(byModified(l)))
default:
// If not one of the above, do nothing
return
}
} else { // If we had more Orderings we could add them here
switch l.Sort {
case "name":
sort.Sort(byName(l))
case "size":
sort.Sort(bySize(l))
case "modified":
sort.Sort(byModified(l))
default:
sort.Sort(byName(l))
return
}
}
}
// Implement sorting for Listing
type byName Listing
type bySize Listing
type byModified Listing
// By Name
func (l byName) Len() int {
return len(l.Items)
}
func (l byName) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
// Treat upper and lower case equally
func (l byName) Less(i, j int) bool {
if l.Items[i].IsDir && !l.Items[j].IsDir {
return true
}
if !l.Items[i].IsDir && l.Items[j].IsDir {
return false
}
return natural.Less(l.Items[i].Name, l.Items[j].Name)
}
// By Size
func (l bySize) Len() int {
return len(l.Items)
}
func (l bySize) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
const directoryOffset = -1 << 31 // = math.MinInt32
func (l bySize) Less(i, j int) bool {
iSize, jSize := l.Items[i].Size, l.Items[j].Size
if l.Items[i].IsDir {
iSize = directoryOffset + iSize
}
if l.Items[j].IsDir {
jSize = directoryOffset + jSize
}
return iSize < jSize
}
// By Modified
func (l byModified) Len() int {
return len(l.Items)
}
func (l byModified) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
func (l byModified) Less(i, j int) bool {
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
return iModified.Sub(jModified) < 0
}
// textExtensions is the sorted list of text extensions which
// can be edited.
var textExtensions = []string{
".ad", ".ada", ".adoc", ".asciidoc",
".bas", ".bash", ".bat",
".c", ".cc", ".cmd", ".conf", ".cpp", ".cr", ".cs", ".css", ".csv",
".d",
".f", ".f90",
".h", ".hh", ".hpp", ".htaccess", ".html",
".ini",
".java", ".js", ".json",
".markdown", ".md", ".mdown", ".mmark",
".nim",
".php", ".pl", ".ps1", ".py",
".rss", ".rst", ".rtf",
".sass", ".scss", ".sh", ".sty",
".tex", ".tml", ".toml", ".txt",
".vala", ".vapi",
".xml",
".yaml", ".yml",
"Caddyfile",
}
// isInTextExtensions checks if a file can be edited by its extensions.
func isInTextExtensions(name string) bool {
search := filepath.Ext(name)
if search == "" {
search = name
}
i := sort.SearchStrings(textExtensions, search)
return i < len(textExtensions) && textExtensions[i] == search
}
// hasRune checks if the file has the frontmatter rune
func hasRune(file string) bool {
return strings.HasPrefix(file, "---") ||
strings.HasPrefix(file, "+++") ||
strings.HasPrefix(file, "{")
}
func editorMode(language string) string {
switch language {
case "markdown", "asciidoc", "rst":
return "content+metadata"
}
return "content"
}
func editorLanguage(mode string) string {
mode = strings.TrimPrefix(mode, ".")
switch mode {
case "md", "markdown", "mdown", "mmark":
mode = "markdown"
case "yml":
mode = "yaml"
case "asciidoc", "adoc", "ad":
mode = "asciidoc"
case "rst":
mode = "rst"
case "html", "htm", "xml":
mode = "htmlmixed"
case "js":
mode = "javascript"
case "go":
mode = "golang"
case "":
mode = "text"
}
return mode
}

View File

@@ -1,579 +0,0 @@
package lib
import (
"crypto/rand"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"reflect"
"regexp"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
rice "github.com/GeertJohan/go.rice"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/robfig/cron"
"github.com/spf13/viper"
)
const (
// Version is the current File Browser version.
Version = "v1.11.0"
ListViewMode = "list"
MosaicViewMode = "mosaic"
)
var (
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyRequest = errors.New("request body is empty")
ErrEmptyPassword = errors.New("password is empty")
ErrEmptyUsername = errors.New("username is empty")
ErrEmptyScope = errors.New("scope is empty")
ErrWrongDataType = errors.New("wrong data type")
ErrInvalidUpdateField = errors.New("invalid field to update")
ErrInvalidOption = errors.New("invalid option")
)
// ReCaptcha settings.
type ReCaptcha struct {
Host string
Key string
Secret string
}
// Auth settings.
type Auth struct {
// Define if which of the following authentication mechansims should be used:
// - 'default', which requires a user and a password.
// - 'proxy', which requires a valid user and the user name has to be provided through an
// http header.
// - 'none', which allows anyone to access the filebrowser instance.
Method string
// If 'Method' is set to 'proxy' the header configured below is used to identify the user.
Header string
}
// FileBrowser is a file manager instance. It should be creating using the
// 'New' function and not directly.
type FileBrowser struct {
// Cron job to manage schedulings.
Cron *cron.Cron
// The key used to sign the JWT tokens.
Key []byte
// The static assets.
Assets *rice.Box
// The Store is used to manage users, shareable links and
// other stuff that is saved on the database.
Store *Store
// PrefixURL is a part of the URL that is already trimmed from the request URL before it
// arrives to our handlers. It may be useful when using File Browser as a middleware
// such as in caddy-filemanager plugin. It is only useful in certain situations.
PrefixURL string
// BaseURL is the path where the GUI will be accessible. It musn't end with
// a trailing slash and mustn't contain PrefixURL, if set. It shouldn't be
// edited directly. Use SetBaseURL.
BaseURL string
// Authentication configuration.
Auth *Auth
// ReCaptcha host, key and secret.
ReCaptcha *ReCaptcha
// StaticGen is the static websit generator handler.
StaticGen StaticGen
// The Default User needed to build the New User page.
DefaultUser *User
// A map of events to a slice of commands.
Commands map[string][]string
// Global stylesheet.
CSS string
// NewFS should build a new file system for a given path.
NewFS FSBuilder
}
var commandEvents = []string{
"before_save",
"after_save",
"before_publish",
"after_publish",
"before_copy",
"after_copy",
"before_rename",
"after_rename",
"before_upload",
"after_upload",
"before_delete",
"after_delete",
}
// Command is a command function.
type Command func(r *http.Request, m *FileBrowser, u *User) error
// FSBuilder is the File System Builder.
type FSBuilder func(scope string) FileSystem
// Setup loads the configuration from the database and configures
// the Assets and the Cron job. It must always be run after
// creating a File Browser object.
func (m *FileBrowser) Setup() error {
// Creates a new File Browser instance with the Users
// map and Assets box.
m.Assets = rice.MustFindBox("../frontend/dist")
m.Cron = cron.New()
// Tries to get the encryption key from the database.
// If it doesn't exist, create a new one of 256 bits.
err := m.Store.Config.Get("key", &m.Key)
if err != nil && err == ErrNotExist {
var bytes []byte
bytes, err = GenerateRandomBytes(64)
if err != nil {
return err
}
m.Key = bytes
err = m.Store.Config.Save("key", m.Key)
}
if err != nil {
return err
}
// Get the global CSS.
err = m.Store.Config.Get("css", &m.CSS)
if err != nil && err == ErrNotExist {
err = m.Store.Config.Save("css", "")
}
if err != nil {
return err
}
// Tries to get the event commands from the database.
// If they don't exist, initialize them.
err = m.Store.Config.Get("commands", &m.Commands)
if err == nil {
// Add hypothetically new command handlers.
for _, command := range commandEvents {
if _, ok := m.Commands[command]; ok {
continue
}
m.Commands[command] = []string{}
}
}
if err != nil && err == ErrNotExist {
m.Commands = map[string][]string{}
// Initialize the command handlers.
for _, command := range commandEvents {
m.Commands[command] = []string{}
}
err = m.Store.Config.Save("commands", m.Commands)
}
if err != nil {
return err
}
// Tries to fetch the users from the database.
users, err := m.Store.Users.Gets(m.NewFS)
if err != nil && err != ErrNotExist {
return err
}
// If there are no users in the database, it creates a new one
// based on 'base' User that must be provided by the function caller.
if len(users) == 0 {
viper.SetDefault("DEFAULT_USERNAME", "admin")
// Hashes the password.
defaultPassword, err := HashPassword("admin")
if err != nil {
return err
}
viper.SetDefault("DEFAULT_PASSWORD_HASH", defaultPassword)
u := *m.DefaultUser
u.Username = viper.GetString("DEFAULT_USERNAME")
u.Password = viper.GetString("DEFAULT_PASSWORD_HASH")
// The first user must be an administrator.
u.Admin = true
u.AllowCommands = true
u.AllowNew = true
u.AllowEdit = true
u.AllowPublish = true
// Saves the user to the database.
if err := m.Store.Users.Save(&u); err != nil {
return err
}
}
m.DefaultUser.Username = ""
m.DefaultUser.Password = ""
m.Cron.AddFunc("@hourly", m.ShareCleaner)
m.Cron.Start()
return nil
}
// RootURL returns the actual URL where
// File Browser interface can be accessed.
func (m FileBrowser) RootURL() string {
return m.PrefixURL + m.BaseURL
}
// SetPrefixURL updates the prefixURL of a File
// Manager object.
func (m *FileBrowser) SetPrefixURL(url string) {
url = strings.TrimPrefix(url, "/")
url = strings.TrimSuffix(url, "/")
url = "/" + url
m.PrefixURL = strings.TrimSuffix(url, "/")
}
// SetBaseURL updates the baseURL of a File Browser
// object.
func (m *FileBrowser) SetBaseURL(url string) {
url = strings.TrimPrefix(url, "/")
url = strings.TrimSuffix(url, "/")
url = "/" + url
m.BaseURL = strings.TrimSuffix(url, "/")
}
// Attach attaches a static generator to the current File Browser.
func (m *FileBrowser) Attach(s StaticGen) error {
if reflect.TypeOf(s).Kind() != reflect.Ptr {
return errors.New("data should be a pointer to interface, not interface")
}
err := s.Setup()
if err != nil {
return err
}
m.StaticGen = s
err = m.Store.Config.Get("staticgen_"+s.Name(), s)
if err == ErrNotExist {
return m.Store.Config.Save("staticgen_"+s.Name(), s)
}
return err
}
// ShareCleaner removes sharing links that are no longer active.
// This function is set to run periodically.
func (m FileBrowser) ShareCleaner() {
// Get all links.
links, err := m.Store.Share.Gets()
if err != nil {
log.Print(err)
return
}
// Find the expired ones.
for i := range links {
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
err = m.Store.Share.Delete(links[i].Hash)
if err != nil {
log.Print(err)
}
}
}
}
// Runner runs the commands for a certain event type.
func (m FileBrowser) Runner(event string, path string, destination string, user *User) error {
commands := []string{}
// Get the commands from the File Browser instance itself.
if val, ok := m.Commands[event]; ok {
commands = append(commands, val...)
}
// Execute the commands.
for _, command := range commands {
args := strings.Split(command, " ")
nonblock := false
if len(args) > 1 && args[len(args)-1] == "&" {
// Run command in background; non-blocking
nonblock = true
args = args[:len(args)-1]
}
command, args, err := caddy.SplitCommandAndArgs(strings.Join(args, " "))
if err != nil {
return err
}
cmd := exec.Command(command, args...)
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
cmd.Env = append(cmd.Env, fmt.Sprintf("ROOT=%s", user.Scope))
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", event))
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
if destination != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", destination))
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if nonblock {
log.Printf("[INFO] Nonblocking Command:\"%s %s\"", command, strings.Join(args, " "))
if err := cmd.Start(); err != nil {
return err
}
continue
}
log.Printf("[INFO] Blocking Command:\"%s %s\"", command, strings.Join(args, " "))
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}
// DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
LockPassword: false,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "",
Scope: ".",
FileSystem: fileutils.Dir("."),
ViewMode: "mosaic",
}
// User contains the configuration for each user.
type User struct {
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// These indicate if the user can perform certain actions.
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowNew bool `json:"allowNew"` // Create files and folders
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Prevents the user to change its password.
LockPassword bool `json:"lockPassword"`
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
// Custom styles for this user.
CSS string `json:"css"`
// FileSystem is the virtual file system the user has access.
FileSystem FileSystem `json:"-"`
// Locale is the language of the user.
Locale string `json:"locale"`
// The hashed password. This never reaches the front-end because it's temporarily
// emptied during JSON marshall.
Password string `json:"password"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Scope is the path the user has access to.
Scope string `json:"filesystem"`
// Username is the user username used to login.
Username string `json:"username" storm:"index,unique"`
// User view mode for files and folders.
ViewMode string `json:"viewMode"`
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule
i := len(u.Rules) - 1
for i >= 0 {
rule = u.Rules[i]
if rule.Regex {
if rule.Regexp.MatchString(url) {
return rule.Allow
}
} else if strings.HasPrefix(url, rule.Path) {
return rule.Allow
}
i--
}
return true
}
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
// MatchString checks if this string matches the regular expression.
func (r *Regexp) MatchString(s string) bool {
if r.regexp == nil {
r.regexp = regexp.MustCompile(r.Raw)
}
return r.regexp.MatchString(s)
}
// ShareLink is the information needed to build a shareable link.
type ShareLink struct {
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
Expires bool `json:"expires"`
ExpireDate time.Time `json:"expireDate"`
}
// Store is a collection of the stores needed to get
// and save information.
type Store struct {
Users UsersStore
Config ConfigStore
Share ShareStore
}
// UsersStore is the interface to manage users.
type UsersStore interface {
Get(id int, builder FSBuilder) (*User, error)
GetByUsername(username string, builder FSBuilder) (*User, error)
Gets(builder FSBuilder) ([]*User, error)
Save(u *User) error
Update(u *User, fields ...string) error
Delete(id int) error
}
// ConfigStore is the interface to manage configuration.
type ConfigStore interface {
Get(name string, to interface{}) error
Save(name string, from interface{}) error
}
// ShareStore is the interface to manage share links.
type ShareStore interface {
Get(hash string) (*ShareLink, error)
GetPermanent(path string) (*ShareLink, error)
GetByPath(path string) ([]*ShareLink, error)
Gets() ([]*ShareLink, error)
Save(s *ShareLink) error
Delete(hash string) error
}
// StaticGen is a static website generator.
type StaticGen interface {
SettingsPath() string
Name() string
Setup() error
Hook(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
}
// FileSystem is the interface to work with the file system.
type FileSystem interface {
Mkdir(name string, perm os.FileMode) error
OpenFile(name string, flag int, perm os.FileMode) (*os.File, error)
RemoveAll(name string) error
Rename(oldName, newName string) error
Stat(name string) (os.FileInfo, error)
Copy(src, dst string) error
}
// Context contains the needed information to make handlers work.
type Context struct {
*FileBrowser
User *User
File *File
// On API handlers, Router is the APi handler we want.
Router string
}
// HashPassword generates an hash from a password using bcrypt.
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPasswordHash compares a password with an hash to check if they match.
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// GenerateRandomBytes returns securely generated random bytes.
// It will return an fm.Error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}

View File

@@ -1,218 +0,0 @@
package http
import (
"encoding/json"
"net/http"
"net/url"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
fb "github.com/filebrowser/filebrowser/lib"
)
const reCaptchaAPI = "/recaptcha/api/siteverify"
type cred struct {
Password string `json:"password"`
Username string `json:"username"`
ReCaptcha string `json:"recaptcha"`
}
// reCaptcha checks the reCaptcha code.
func reCaptcha(host, secret, response string) (bool, error) {
body := url.Values{}
body.Set("secret", secret)
body.Add("response", response)
client := &http.Client{}
resp, err := client.Post(host+reCaptchaAPI, "application/x-www-form-urlencoded", strings.NewReader(body.Encode()))
if err != nil {
return false, err
}
if resp.StatusCode != http.StatusOK {
return false, nil
}
var data struct {
Success bool `json:"success"`
}
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return false, err
}
return data.Success, nil
}
// authHandler processes the authentication for the user.
func authHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if c.Auth.Method == "none" {
// NoAuth instances shouldn't call this method.
return 0, nil
}
if c.Auth.Method == "proxy" {
// Receive the Username from the Header and check if it exists.
u, err := c.Store.Users.GetByUsername(r.Header.Get(c.Auth.Header), c.NewFS)
if err != nil {
return http.StatusForbidden, nil
}
c.User = u
return printToken(c, w)
}
// Receive the credentials from the request and unmarshal them.
var cred cred
if r.Body == nil {
return http.StatusForbidden, nil
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
return http.StatusForbidden, err
}
// If ReCaptcha is enabled, check the code.
if len(c.ReCaptcha.Secret) > 0 {
ok, err := reCaptcha(c.ReCaptcha.Host, c.ReCaptcha.Secret, cred.ReCaptcha)
if err != nil {
return http.StatusForbidden, err
}
if !ok {
return http.StatusForbidden, nil
}
}
// Checks if the user exists.
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
if err != nil {
return http.StatusForbidden, nil
}
// Checks if the password is correct.
if !fb.CheckPasswordHash(cred.Password, u.Password) {
return http.StatusForbidden, nil
}
c.User = u
return printToken(c, w)
}
// renewAuthHandler is used when the front-end already has a JWT token
// and is checking if it is up to date. If so, updates its info.
func renewAuthHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
ok, u := validateAuth(c, r)
if !ok {
return http.StatusForbidden, nil
}
c.User = u
return printToken(c, w)
}
// claims is the JWT claims.
type claims struct {
fb.User
jwt.StandardClaims
}
// printToken prints the final JWT token to the user.
func printToken(c *fb.Context, w http.ResponseWriter) (int, error) {
// Creates a copy of the user and removes it password
// hash so it never arrives to the user.
u := fb.User{}
u = *c.User
u.Password = ""
// Builds the claims.
claims := claims{
u,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
Issuer: "File Browser",
},
}
// Creates the token and signs it.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(c.Key)
if err != nil {
return http.StatusInternalServerError, err
}
// Writes the token.
w.Header().Set("Content-Type", "cty")
w.Write([]byte(signed))
return 0, nil
}
type extractor []string
func (e extractor) ExtractToken(r *http.Request) (string, error) {
token, _ := request.AuthorizationHeaderExtractor.ExtractToken(r)
// Checks if the token isn't empty and if it contains two dots.
// The former prevents incompatibility with URLs that previously
// used basic auth.
if token != "" && strings.Count(token, ".") == 2 {
return token, nil
}
cookie, err := r.Cookie("auth")
if err != nil {
return "", request.ErrNoTokenInRequest
}
return cookie.Value, nil
}
// validateAuth is used to validate the authentication and returns the
// User if it is valid.
func validateAuth(c *fb.Context, r *http.Request) (bool, *fb.User) {
if c.Auth.Method == "none" {
c.User = c.DefaultUser
return true, c.User
}
// If proxy auth is used do not verify the JWT token if the header is provided.
if c.Auth.Method == "proxy" {
u, err := c.Store.Users.GetByUsername(r.Header.Get(c.Auth.Header), c.NewFS)
if err != nil {
return false, nil
}
c.User = u
return true, c.User
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
return c.Key, nil
}
var claims claims
token, err := request.ParseFromRequestWithClaims(r,
extractor{},
&claims,
keyFunc,
)
if err != nil || !token.Valid {
return false, nil
}
u, err := c.Store.Users.Get(claims.User.ID, c.NewFS)
if err != nil {
return false, nil
}
c.User = u
return true, u
}

View File

@@ -1,102 +0,0 @@
package http
import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
fb "github.com/filebrowser/filebrowser/lib"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
)
// downloadHandler creates an archive in one of the supported formats (zip, tar,
// tar.gz or tar.bz2) and sends it to be downloaded.
func downloadHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If the file isn't a directory, serve it using http.ServeFile. We display it
// inline if it is requested.
if !c.File.IsDir {
return downloadFileHandler(c, w, r)
}
query := r.URL.Query().Get("format")
files := []string{}
names := strings.Split(r.URL.Query().Get("files"), ",")
// If there are files in the query, sanitize their names.
// Otherwise, just append the current path.
if len(names) != 0 {
for _, name := range names {
// Unescape the name.
name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1))
if err != nil {
return http.StatusInternalServerError, err
}
// Clean the slashes.
name = fileutils.SlashClean(name)
files = append(files, filepath.Join(c.File.Path, name))
}
} else {
files = append(files, c.File.Path)
}
var (
extension string
ar archiver.Archiver
)
switch query {
// If the format is true, just set it to "zip".
case "zip", "true", "":
extension, ar = ".zip", archiver.Zip
case "tar":
extension, ar = ".tar", archiver.Tar
case "targz":
extension, ar = ".tar.gz", archiver.TarGz
case "tarbz2":
extension, ar = ".tar.bz2", archiver.TarBz2
case "tarxz":
extension, ar = ".tar.xz", archiver.TarXZ
default:
return http.StatusNotImplemented, nil
}
// Defines the file name.
name := c.File.Name
if name == "." || name == "" {
name = "archive"
}
name += extension
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
err := ar.Write(w, files)
return 0, err
}
func downloadFileHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
file, err := os.Open(c.File.Path)
if err != nil {
return http.StatusInternalServerError, err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return http.StatusInternalServerError, err
}
if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline")
} else {
// As per RFC6266 section 4.3
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(c.File.Name))
}
http.ServeContent(w, r, stat.Name(), stat.ModTime(), file)
return 0, nil
}

View File

@@ -1,348 +0,0 @@
package http
import (
"encoding/json"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
fb "github.com/filebrowser/filebrowser/lib"
)
// Handler returns a function compatible with http.HandleFunc.
func Handler(m *fb.FileBrowser) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
code, err := serve(&fb.Context{
FileBrowser: m,
User: nil,
File: nil,
}, w, r)
if code >= 400 {
w.WriteHeader(code)
txt := http.StatusText(code)
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
w.Write([]byte(txt + "\n"))
}
if err != nil {
log.Print(err)
}
})
}
// serve is the main entry point of this HTML application.
func serve(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
// returns a 404 fb.Error because we're not supposed to be here!
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
return http.StatusNotFound, nil
}
r.URL.Path = p
// Check if this request is made to the service worker. If so,
// pass it through a template to add the needed variables.
if r.URL.Path == "/sw.js" {
return renderFile(c, w, "sw.js")
}
// Checks if this request is made to the static assets folder. If so, and
// if it is a GET request, returns with the asset. Otherwise, returns
// a status not implemented.
if matchURL(r.URL.Path, "/static") {
if r.Method != http.MethodGet {
return http.StatusNotImplemented, nil
}
return staticHandler(c, w, r)
}
// Checks if this request is made to the API and directs to the
// API handler if so.
if matchURL(r.URL.Path, "/api") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
return apiHandler(c, w, r)
}
// If it is a request to the preview and a static website generator is
// active, build the preview.
if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview")
return c.StaticGen.Preview(c, w, r)
}
if strings.HasPrefix(r.URL.Path, "/share/") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
return sharePage(c, w, r)
}
// Any other request should show the index.html file.
w.Header().Set("x-frame-options", "SAMEORIGIN")
w.Header().Set("x-content-type-options", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block")
return renderFile(c, w, "index.html")
}
// staticHandler handles the static assets path.
func staticHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil
}
return renderFile(c, w, "static/manifest.json")
}
// apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth/get" {
return authHandler(c, w, r)
}
if r.URL.Path == "/auth/renew" {
return renewAuthHandler(c, w, r)
}
valid, _ := validateAuth(c, r)
if !valid {
return http.StatusForbidden, nil
}
c.Router, r.URL.Path = splitURL(r.URL.Path)
if !c.User.Allowed(r.URL.Path) {
return http.StatusForbidden, nil
}
if c.StaticGen != nil {
// If we are using the 'magic url' for the settings,
// we should redirect the request for the acutual path.
if r.URL.Path == "/settings" {
r.URL.Path = c.StaticGen.SettingsPath()
}
// Executes the Static website generator hook.
code, err := c.StaticGen.Hook(c, w, r)
if code != 0 || err != nil {
return code, err
}
}
if c.Router == "checksum" || c.Router == "download" || c.Router == "subtitle" || c.Router == "subtitles" {
var err error
c.File, err = fb.GetInfo(r.URL, c.FileBrowser, c.User)
if err != nil {
return ErrorToHTTP(err, false), err
}
}
var code int
var err error
switch c.Router {
case "download":
code, err = downloadHandler(c, w, r)
case "checksum":
code, err = checksumHandler(c, w, r)
case "command":
code, err = command(c, w, r)
case "search":
code, err = search(c, w, r)
case "resource":
code, err = resourceHandler(c, w, r)
case "users":
code, err = usersHandler(c, w, r)
case "settings":
code, err = settingsHandler(c, w, r)
case "share":
code, err = shareHandler(c, w, r)
case "subtitles":
code, err = subtitlesHandler(c, w, r)
case "subtitle":
code, err = subtitleHandler(c, w, r)
default:
code = http.StatusNotFound
}
return code, err
}
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func checksumHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo")
val, err := c.File.Checksum(query)
if err == fb.ErrInvalidOption {
return http.StatusBadRequest, err
} else if err != nil {
return http.StatusInternalServerError, err
}
w.Write([]byte(val))
return 0, nil
}
// splitURL splits the path and returns everything that stands
// before the first slash and everything that goes after.
func splitURL(path string) (string, string) {
if path == "" {
return "", ""
}
path = strings.TrimPrefix(path, "/")
i := strings.Index(path, "/")
if i == -1 {
return "", path
}
return path[0:i], path[i:]
}
// renderFile renders a file using a template with some needed variables.
func renderFile(c *fb.Context, w http.ResponseWriter, file string) (int, error) {
tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file)))
var contentType string
switch filepath.Ext(file) {
case ".html":
contentType = "text/html"
case ".js":
contentType = "application/javascript"
case ".json":
contentType = "application/json"
default:
contentType = "text"
}
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
data := map[string]interface{}{
"baseurl": c.RootURL(),
"NoAuth": c.Auth.Method == "none",
"Version": fb.Version,
"CSS": template.CSS(c.CSS),
"ReCaptcha": c.ReCaptcha.Key != "" && c.ReCaptcha.Secret != "",
"ReCaptchaHost": c.ReCaptcha.Host,
"ReCaptchaKey": c.ReCaptcha.Key,
}
if c.StaticGen != nil {
data["staticgen"] = c.StaticGen.Name()
}
err := tpl.Execute(w, data)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// sharePage build the share page.
func sharePage(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
s, err := c.Store.Share.Get(r.URL.Path)
if err == fb.ErrNotExist {
w.WriteHeader(http.StatusNotFound)
return renderFile(c, w, "static/share/404.html")
}
if err != nil {
return http.StatusInternalServerError, err
}
if s.Expires && s.ExpireDate.Before(time.Now()) {
c.Store.Share.Delete(s.Hash)
w.WriteHeader(http.StatusNotFound)
return renderFile(c, w, "static/share/404.html")
}
r.URL.Path = s.Path
info, err := os.Stat(s.Path)
if err != nil {
c.Store.Share.Delete(s.Hash)
return ErrorToHTTP(err, false), err
}
c.File = &fb.File{
Path: s.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
}
dl := r.URL.Query().Get("dl")
if dl == "" || dl == "0" {
tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html")))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{
"baseurl": c.RootURL(),
"File": c.File,
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
return downloadHandler(c, w, r)
}
// renderJSON prints the JSON version of data to the browser.
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
marsh, err := json.Marshal(data)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write(marsh); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// matchURL checks if the first URL matches the second.
func matchURL(first, second string) bool {
first = strings.ToLower(first)
second = strings.ToLower(second)
return strings.HasPrefix(first, second)
}
// ErrorToHTTP converts errors to HTTP Status Code.
func ErrorToHTTP(err error, gone bool) int {
switch {
case err == nil:
return http.StatusOK
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err):
if !gone {
return http.StatusNotFound
}
return http.StatusGone
case os.IsExist(err):
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}

View File

@@ -1,386 +0,0 @@
package http
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
fb "github.com/filebrowser/filebrowser/lib"
"github.com/hacdias/fileutils"
)
// sanitizeURL sanitizes the URL to prevent path transversal
// using fileutils.SlashClean and adds the trailing slash bar.
func sanitizeURL(url string) string {
path := fileutils.SlashClean(url)
if strings.HasSuffix(url, "/") && path != "/" {
return path + "/"
}
return path
}
func resourceHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
case http.MethodGet:
return resourceGetHandler(c, w, r)
case http.MethodDelete:
return resourceDeleteHandler(c, w, r)
case http.MethodPut:
// Before save command handler.
path := filepath.Join(c.User.Scope, r.URL.Path)
if err := c.Runner("before_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
code, err := resourcePostPutHandler(c, w, r)
if code != http.StatusOK {
return code, err
}
// After save command handler.
if err := c.Runner("after_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return code, err
case http.MethodPatch:
return resourcePatchHandler(c, w, r)
case http.MethodPost:
return resourcePostPutHandler(c, w, r)
}
return http.StatusNotImplemented, nil
}
func resourceGetHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file.
f, err := fb.GetInfo(r.URL, c.FileBrowser, c.User)
if err != nil {
return ErrorToHTTP(err, false), err
}
// If it's a dir and the path doesn't end with a trailing slash,
// add a trailing slash to the path.
if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") {
r.URL.Path = r.URL.Path + "/"
}
// If it is a dir, go and serve the listing.
if f.IsDir {
c.File = f
return listingHandler(c, w, r)
}
// Tries to get the file type.
if err = f.GetFileType(true); err != nil {
return ErrorToHTTP(err, true), err
}
// Serve a preview if the file can't be edited or the
// user has no permission to edit this file. Otherwise,
// just serve the editor.
if !f.CanBeEdited() || !c.User.AllowEdit {
f.Kind = "preview"
return renderJSON(w, f)
}
f.Kind = "editor"
// Tries to get the editor data.
if err = f.GetEditor(); err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, f)
}
func listingHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.File
f.Kind = "listing"
// Tries to get the listing data.
if err := f.GetListing(c.User, r); err != nil {
return ErrorToHTTP(err, true), err
}
listing := f.Listing
// Defines the cookie scope.
cookieScope := c.RootURL()
if cookieScope == "" {
cookieScope = "/"
}
// Copy the query values into the Listing struct
if sort, order, err := handleSortOrder(w, r, cookieScope); err == nil {
listing.Sort = sort
listing.Order = order
} else {
return http.StatusBadRequest, err
}
listing.ApplySort()
return renderJSON(w, f)
}
func resourceDeleteHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Prevent the removal of the root directory.
if r.URL.Path == "/" || !c.User.AllowEdit {
return http.StatusForbidden, nil
}
// Fire the before trigger.
if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
// Remove the file or folder.
err := c.User.FileSystem.RemoveAll(r.URL.Path)
if err != nil {
return ErrorToHTTP(err, true), err
}
// Fire the after trigger.
if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func resourcePostPutHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowNew && r.Method == http.MethodPost {
return http.StatusForbidden, nil
}
if !c.User.AllowEdit && r.Method == http.MethodPut {
return http.StatusForbidden, nil
}
// Discard any invalid upload before returning to avoid connection
// reset error.
defer func() {
io.Copy(ioutil.Discard, r.Body)
}()
// Checks if the current request is for a directory and not a file.
if strings.HasSuffix(r.URL.Path, "/") {
// If the method is PUT, we return 405 Method not Allowed, because
// POST should be used instead.
if r.Method == http.MethodPut {
return http.StatusMethodNotAllowed, nil
}
// Otherwise we try to create the directory.
err := c.User.FileSystem.Mkdir(r.URL.Path, 0775)
return ErrorToHTTP(err, false), err
}
// If using POST method, we are trying to create a new file so it is not
// desirable to override an already existent file. Thus, we check
// if the file already exists. If so, we just return a 409 Conflict.
if r.Method == http.MethodPost && r.Header.Get("Action") != "override" {
if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil {
return http.StatusConflict, errors.New("There is already a file on that path")
}
}
// Fire the before trigger.
if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
// Create/Open the file.
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil {
return ErrorToHTTP(err, false), err
}
defer f.Close()
// Copies the new content for the file.
_, err = io.Copy(f, r.Body)
if err != nil {
return ErrorToHTTP(err, false), err
}
// Gets the info about the file.
fi, err := f.Stat()
if err != nil {
return ErrorToHTTP(err, false), err
}
// Check if this instance has a Static Generator and handles publishing
// or scheduling if it's the case.
if c.StaticGen != nil {
code, err := resourcePublishSchedule(c, w, r)
if code != 0 {
return code, err
}
}
// Writes the ETag Header.
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
w.Header().Set("ETag", etag)
// Fire the after trigger.
if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func resourcePublishSchedule(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
publish := r.Header.Get("Publish")
schedule := r.Header.Get("Schedule")
if publish != "true" && schedule == "" {
return 0, nil
}
if !c.User.AllowPublish {
return http.StatusForbidden, nil
}
if publish == "true" {
return resourcePublish(c, w, r)
}
t, err := time.Parse("2006-01-02T15:04", schedule)
if err != nil {
return http.StatusInternalServerError, err
}
c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
_, err := resourcePublish(c, w, r)
if err != nil {
log.Print(err)
}
})
return http.StatusOK, nil
}
func resourcePublish(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path)
// Before save command handler.
if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
code, err := c.StaticGen.Publish(c, w, r)
if err != nil {
return code, err
}
// Executed the before publish command.
if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return code, nil
}
// resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit {
return http.StatusForbidden, nil
}
dst := r.Header.Get("Destination")
action := r.Header.Get("Action")
dst, err := url.QueryUnescape(dst)
if err != nil {
return ErrorToHTTP(err, true), err
}
src := r.URL.Path
if dst == "/" || src == "/" {
return http.StatusForbidden, nil
}
if action == "copy" {
// Fire the after trigger.
if err := c.Runner("before_copy", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
// Copy the file.
err = c.User.FileSystem.Copy(src, dst)
// Fire the after trigger.
if err := c.Runner("after_copy", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
} else {
// Fire the after trigger.
if err := c.Runner("before_rename", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
// Rename the file.
err = c.User.FileSystem.Rename(src, dst)
// Fire the after trigger.
if err := c.Runner("after_rename", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
}
return ErrorToHTTP(err, true), err
}
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
// and reads 'limit' if given. The latter is 0 if not given. Sets cookies.
func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) {
sort = r.URL.Query().Get("sort")
order = r.URL.Query().Get("order")
// If the query 'sort' or 'order' is empty, use defaults or any values
// previously saved in Cookies.
switch sort {
case "":
sort = "name"
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
sort = sortCookie.Value
}
case "name", "size":
http.SetCookie(w, &http.Cookie{
Name: "sort",
Value: sort,
MaxAge: 31536000,
Path: scope,
Secure: r.TLS != nil,
})
}
switch order {
case "":
order = "asc"
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
order = orderCookie.Value
}
case "asc", "desc":
http.SetCookie(w, &http.Cookie{
Name: "order",
Value: order,
MaxAge: 31536000,
Path: scope,
Secure: r.TLS != nil,
})
}
return
}

View File

@@ -1,146 +0,0 @@
package http
import (
"bytes"
"encoding/json"
"net/http"
"reflect"
fb "github.com/filebrowser/filebrowser/lib"
"github.com/mitchellh/mapstructure"
)
type modifySettingsRequest struct {
modifyRequest
Data struct {
CSS string `json:"css"`
Commands map[string][]string `json:"commands"`
StaticGen map[string]interface{} `json:"staticGen"`
} `json:"data"`
}
type option struct {
Variable string `json:"variable"`
Name string `json:"name"`
Value interface{} `json:"value"`
}
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, fb.ErrEmptyRequest
}
// Parses the request body and checks if it's well formed.
mod := &modifySettingsRequest{}
err := json.NewDecoder(r.Body).Decode(mod)
if err != nil {
return nil, err
}
// Checks if the request type is right.
if mod.What != "settings" {
return nil, fb.ErrWrongDataType
}
return mod, nil
}
func settingsHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "" && r.URL.Path != "/" {
return http.StatusNotFound, nil
}
switch r.Method {
case http.MethodGet:
return settingsGetHandler(c, w, r)
case http.MethodPut:
return settingsPutHandler(c, w, r)
}
return http.StatusMethodNotAllowed, nil
}
type settingsGetRequest struct {
CSS string `json:"css"`
Commands map[string][]string `json:"commands"`
StaticGen []option `json:"staticGen"`
}
func settingsGetHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
result := &settingsGetRequest{
Commands: c.Commands,
StaticGen: []option{},
CSS: c.CSS,
}
if c.StaticGen != nil {
t := reflect.TypeOf(c.StaticGen).Elem()
for i := 0; i < t.NumField(); i++ {
if t.Field(i).Name[0] == bytes.ToLower([]byte{t.Field(i).Name[0]})[0] {
continue
}
result.StaticGen = append(result.StaticGen, option{
Variable: t.Field(i).Name,
Name: t.Field(i).Tag.Get("name"),
Value: reflect.ValueOf(c.StaticGen).Elem().FieldByName(t.Field(i).Name).Interface(),
})
}
}
return renderJSON(w, result)
}
func settingsPutHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
mod, err := parsePutSettingsRequest(r)
if err != nil {
return http.StatusBadRequest, err
}
// Update the commands.
if mod.Which == "commands" {
if err := c.Store.Config.Save("commands", mod.Data.Commands); err != nil {
return http.StatusInternalServerError, err
}
c.Commands = mod.Data.Commands
return http.StatusOK, nil
}
// Update the global CSS.
if mod.Which == "css" {
if err := c.Store.Config.Save("css", mod.Data.CSS); err != nil {
return http.StatusInternalServerError, err
}
c.CSS = mod.Data.CSS
return http.StatusOK, nil
}
// Update the static generator options.
if mod.Which == "staticGen" {
err = mapstructure.Decode(mod.Data.StaticGen, c.StaticGen)
if err != nil {
return http.StatusInternalServerError, err
}
err = c.Store.Config.Save("staticgen_"+c.StaticGen.Name(), c.StaticGen)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
return http.StatusMethodNotAllowed, nil
}

View File

@@ -1,127 +0,0 @@
package http
import (
"encoding/base64"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
fb "github.com/filebrowser/filebrowser/lib"
)
func shareHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
case http.MethodGet:
return shareGetHandler(c, w, r)
case http.MethodDelete:
return shareDeleteHandler(c, w, r)
case http.MethodPost:
return sharePostHandler(c, w, r)
}
return http.StatusNotImplemented, nil
}
func shareGetHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path)
s, err := c.Store.Share.GetByPath(path)
if err == fb.ErrNotExist {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
for i, link := range s {
if link.Expires && link.ExpireDate.Before(time.Now()) {
c.Store.Share.Delete(link.Hash)
s = append(s[:i], s[i+1:]...)
}
}
if len(s) == 0 {
return http.StatusNotFound, nil
}
return renderJSON(w, s)
}
func sharePostHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path)
var s *fb.ShareLink
expire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit")
if expire == "" {
var err error
s, err = c.Store.Share.GetPermanent(path)
if err == nil {
w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
return 0, nil
}
}
bytes, err := fb.GenerateRandomBytes(6)
if err != nil {
return http.StatusInternalServerError, err
}
str := base64.URLEncoding.EncodeToString(bytes)
s = &fb.ShareLink{
Path: path,
Hash: str,
Expires: expire != "",
}
if expire != "" {
num, err := strconv.Atoi(expire)
if err != nil {
return http.StatusInternalServerError, err
}
var add time.Duration
switch unit {
case "seconds":
add = time.Second * time.Duration(num)
case "minutes":
add = time.Minute * time.Duration(num)
case "days":
add = time.Hour * 24 * time.Duration(num)
default:
add = time.Hour * time.Duration(num)
}
s.ExpireDate = time.Now().Add(add)
}
if err := c.Store.Share.Save(s); err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, s)
}
func shareDeleteHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
s, err := c.Store.Share.Get(strings.TrimPrefix(r.URL.Path, "/"))
if err == fb.ErrNotExist {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
err = c.Store.Share.Delete(s.Hash)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}

View File

@@ -1,83 +0,0 @@
package http
import (
"bytes"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"regexp"
fb "github.com/filebrowser/filebrowser/lib"
)
func subtitlesHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
files, err := readDir(filepath.Dir(c.File.Path))
if err != nil {
return http.StatusInternalServerError, err
}
subtitles := make([]map[string]string, 0)
for _, file := range files {
ext := filepath.Ext(file.Name())
if ext == ".vtt" || ext == ".srt" {
sub := make(map[string]string)
sub["src"] = filepath.Dir(c.File.Path) + "/" + file.Name()
sub["kind"] = "subtitles"
sub["label"] = file.Name()
subtitles = append(subtitles, sub)
}
}
return renderJSON(w, subtitles)
}
func subtitleHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
str, err := cleanSubtitle(c.File.Path)
if err != nil {
return http.StatusInternalServerError, err
}
file, err := os.Open(c.File.Path)
if err != nil {
return http.StatusInternalServerError, err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Disposition", "inline")
w.Header().Set("Content-Type", "text/vtt")
http.ServeContent(w, r, stat.Name(), stat.ModTime(), bytes.NewReader([]byte(str)))
return 0, nil
}
func cleanSubtitle(filename string) (string, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
str := string(b) // convert content to a 'string'
ext := filepath.Ext(filename)
if ext == ".srt" {
re := regexp.MustCompile("([0-9]{2}:[0-9]{2}:[0-9]{2}),([0-9]{3})")
str = "WEBVTT\n\n" + re.ReplaceAllString(str, "$1.$2")
}
return str, err
}
func readDir(dirname string) ([]os.FileInfo, error) {
f, err := os.Open(dirname)
if err != nil {
return nil, err
}
list, err := f.Readdir(-1)
f.Close()
if err != nil {
return nil, err
}
return list, nil
}

View File

@@ -1,383 +0,0 @@
package http
import (
"encoding/json"
"errors"
"net/http"
"os"
"sort"
"strconv"
"strings"
fb "github.com/filebrowser/filebrowser/lib"
)
type modifyRequest struct {
What string `json:"what"` // Answer to: what data type?
Which string `json:"which"` // Answer to: which field?
}
type modifyUserRequest struct {
modifyRequest
Data *fb.User `json:"data"`
}
// usersHandler is the entry point of the users API. It's just a router
// to send the request to its
func usersHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If the user isn't admin and isn't making a PUT
// request, then return forbidden.
if !c.User.Admin && r.Method != http.MethodPut {
return http.StatusForbidden, nil
}
switch r.Method {
case http.MethodGet:
return usersGetHandler(c, w, r)
case http.MethodPost:
return usersPostHandler(c, w, r)
case http.MethodDelete:
return usersDeleteHandler(c, w, r)
case http.MethodPut:
return usersPutHandler(c, w, r)
}
return http.StatusNotImplemented, nil
}
// getUserID returns the id from the user which is present
// in the request url. If the url is invalid and doesn't
// contain a valid ID, it returns an fb.Error.
func getUserID(r *http.Request) (int, error) {
// Obtains the ID in string from the URL and converts
// it into an integer.
sid := strings.TrimPrefix(r.URL.Path, "/")
sid = strings.TrimSuffix(sid, "/")
id, err := strconv.Atoi(sid)
if err != nil {
return http.StatusBadRequest, err
}
return id, nil
}
// getUser returns the user which is present in the request
// body. If the body is empty or the JSON is invalid, it
// returns an fb.Error.
func getUser(c *fb.Context, r *http.Request) (*fb.User, string, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, "", fb.ErrEmptyRequest
}
// Parses the request body and checks if it's well formed.
mod := &modifyUserRequest{}
err := json.NewDecoder(r.Body).Decode(mod)
if err != nil {
return nil, "", err
}
// Checks if the request type is right.
if mod.What != "user" {
return nil, "", fb.ErrWrongDataType
}
mod.Data.FileSystem = c.NewFS(mod.Data.Scope)
return mod.Data, mod.Which, nil
}
func usersGetHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Request for the default user data.
if r.URL.Path == "/base" {
return renderJSON(w, c.DefaultUser)
}
// Request for the listing of users.
if r.URL.Path == "/" {
users, err := c.Store.Users.Gets(c.NewFS)
if err != nil {
return http.StatusInternalServerError, err
}
for _, u := range users {
// Removes the user password so it won't
// be sent to the front-end.
u.Password = ""
}
sort.Slice(users, func(i, j int) bool {
return users[i].ID < users[j].ID
})
return renderJSON(w, users)
}
id, err := getUserID(r)
if err != nil {
return http.StatusInternalServerError, err
}
u, err := c.Store.Users.Get(id, c.NewFS)
if err == fb.ErrExist {
return http.StatusNotFound, err
}
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = ""
return renderJSON(w, u)
}
func usersPostHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/" {
return http.StatusMethodNotAllowed, nil
}
u, _, err := getUser(c, r)
if err != nil {
return http.StatusBadRequest, err
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, fb.ErrEmptyUsername
}
// Checks if scope isn't empty.
if u.Scope == "" {
return http.StatusBadRequest, fb.ErrEmptyScope
}
// Checks if password isn't empty.
if u.Password == "" {
return http.StatusBadRequest, fb.ErrEmptyPassword
}
// Initialize rules if they're not initialized.
if u.Rules == nil {
u.Rules = []*fb.Rule{}
}
// If the view mode is empty, initialize with the default one.
if u.ViewMode == "" {
u.ViewMode = c.DefaultUser.ViewMode
}
// Initialize commands if not initialized.
if u.Commands == nil {
u.Commands = []string{}
}
// It's a new user so the ID will be auto created.
if u.ID != 0 {
u.ID = 0
}
// Checks if the scope exists.
if code, err := checkFS(u.Scope); err != nil {
return code, err
}
// Hashes the password.
pw, err := fb.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = pw
u.ViewMode = fb.MosaicViewMode
// Saves the user to the database.
err = c.Store.Users.Save(u)
if err == fb.ErrExist {
return http.StatusConflict, err
}
if err != nil {
return http.StatusInternalServerError, err
}
// Set the Location header and return.
w.Header().Set("Location", "/settings/users/"+strconv.Itoa(u.ID))
w.WriteHeader(http.StatusCreated)
return 0, nil
}
func checkFS(path string) (int, error) {
info, err := os.Stat(path)
if err != nil {
if !os.IsNotExist(err) {
return http.StatusInternalServerError, err
}
err = os.MkdirAll(path, 0666)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
if !info.IsDir() {
return http.StatusBadRequest, errors.New("Scope is not a dir")
}
return 0, nil
}
func usersDeleteHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil
}
id, err := getUserID(r)
if err != nil {
return http.StatusInternalServerError, err
}
// Deletes the user from the database.
err = c.Store.Users.Delete(id)
if err == fb.ErrNotExist {
return http.StatusNotFound, fb.ErrNotExist
}
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func usersPutHandler(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// New users should be created on /api/users.
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil
}
// Gets the user ID from the URL and checks if it's valid.
id, err := getUserID(r)
if err != nil {
return http.StatusInternalServerError, err
}
// Checks if the user has permission to access this page.
if !c.User.Admin && id != c.User.ID {
return http.StatusForbidden, nil
}
// Gets the user from the request body.
u, which, err := getUser(c, r)
if err != nil {
return http.StatusBadRequest, err
}
// If we're updating the default user. Only for NoAuth
// implementations. Used to change the viewMode.
if id == 0 && c.Auth.Method == "none" {
c.DefaultUser.ViewMode = u.ViewMode
return http.StatusOK, nil
}
// Updates the CSS and locale.
if which == "partial" {
c.User.CSS = u.CSS
c.User.Locale = u.Locale
c.User.ViewMode = u.ViewMode
err = c.Store.Users.Update(c.User, "CSS", "Locale", "ViewMode")
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
// Updates the Password.
if which == "password" {
if u.Password == "" {
return http.StatusBadRequest, fb.ErrEmptyPassword
}
if id == c.User.ID && c.User.LockPassword {
return http.StatusForbidden, nil
}
c.User.Password, err = fb.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
err = c.Store.Users.Update(c.User, "Password")
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
// If can only be all.
if which != "all" {
return http.StatusBadRequest, fb.ErrInvalidUpdateField
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, fb.ErrEmptyUsername
}
// Checks if filesystem isn't empty.
if u.Scope == "" {
return http.StatusBadRequest, fb.ErrEmptyScope
}
// Checks if the scope exists.
if code, err := checkFS(u.Scope); err != nil {
return code, err
}
// Initialize rules if they're not initialized.
if u.Rules == nil {
u.Rules = []*fb.Rule{}
}
// Initialize commands if not initialized.
if u.Commands == nil {
u.Commands = []string{}
}
// Gets the current saved user from the in-memory map.
suser, err := c.Store.Users.Get(id, c.NewFS)
if err == fb.ErrNotExist {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
u.ID = id
// Changes the password if the request wants it.
if u.Password != "" {
pw, err := fb.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = pw
} else {
u.Password = suser.Password
}
// Updates the whole User struct because we always are supposed
// to send a new entire object.
err = c.Store.Users.Update(u)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}

View File

@@ -1,349 +0,0 @@
package http
import (
"bytes"
"encoding/json"
"mime"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
fb "github.com/filebrowser/filebrowser/lib"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
var (
cmdNotImplemented = []byte("Command not implemented.")
cmdNotAllowed = []byte("Command not allowed.")
)
// command handles the requests for VCS related commands: git, svn and mercurial
func command(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for fb.Errors.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return 0, err
}
defer conn.Close()
var (
message []byte
command []string
)
// Starts an infinite loop until a valid command is captured.
for {
_, message, err = conn.ReadMessage()
if err != nil {
return http.StatusInternalServerError, err
}
command = strings.Split(string(message), " ")
if len(command) != 0 {
break
}
}
// Check if the command is allowed
allowed := false
for _, cmd := range c.User.Commands {
if regexp.MustCompile(cmd).MatchString(command[0]) {
allowed = true
break
}
}
if !allowed {
err = conn.WriteMessage(websocket.TextMessage, cmdNotAllowed)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// Check if the program is installed on the computer.
if _, err = exec.LookPath(command[0]); err != nil {
err = conn.WriteMessage(websocket.TextMessage, cmdNotImplemented)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusNotImplemented, nil
}
// Gets the path and initializes a buffer.
path := c.User.Scope + "/" + r.URL.Path
path = filepath.Clean(path)
buff := new(bytes.Buffer)
// Sets up the command executation.
cmd := exec.Command(command[0], command[1:]...)
cmd.Dir = path
cmd.Stderr = buff
cmd.Stdout = buff
// Starts the command and checks for fb.Errors.
err = cmd.Start()
if err != nil {
return http.StatusInternalServerError, err
}
// Set a 'done' variable to check whetever the command has already finished
// running or not. This verification is done using a goroutine that uses the
// method .Wait() from the command.
done := false
go func() {
err = cmd.Wait()
done = true
}()
// Function to print the current information on the buffer to the connection.
print := func() error {
by := buff.Bytes()
if len(by) > 0 {
err = conn.WriteMessage(websocket.TextMessage, by)
if err != nil {
return err
}
}
return nil
}
// While the command hasn't finished running, continue sending the output
// to the client in intervals of 100 milliseconds.
for !done {
if err = print(); err != nil {
return http.StatusInternalServerError, err
}
time.Sleep(100 * time.Millisecond)
}
// After the command is done executing, send the output one more time to the
// browser to make sure it gets the latest information.
if err = print(); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
var (
typeRegexp = regexp.MustCompile(`type:(\w+)`)
)
type condition func(path string) bool
type searchOptions struct {
CaseSensitive bool
Conditions []condition
Terms []string
}
func extensionCondition(extension string) condition {
return func(path string) bool {
return filepath.Ext(path) == "."+extension
}
}
func imageCondition(path string) bool {
extension := filepath.Ext(path)
mimetype := mime.TypeByExtension(extension)
return strings.HasPrefix(mimetype, "image")
}
func audioCondition(path string) bool {
extension := filepath.Ext(path)
mimetype := mime.TypeByExtension(extension)
return strings.HasPrefix(mimetype, "audio")
}
func videoCondition(path string) bool {
extension := filepath.Ext(path)
mimetype := mime.TypeByExtension(extension)
return strings.HasPrefix(mimetype, "video")
}
func parseSearch(value string) *searchOptions {
opts := &searchOptions{
CaseSensitive: strings.Contains(value, "case:sensitive"),
Conditions: []condition{},
Terms: []string{},
}
// removes the options from the value
value = strings.Replace(value, "case:insensitive", "", -1)
value = strings.Replace(value, "case:sensitive", "", -1)
value = strings.TrimSpace(value)
types := typeRegexp.FindAllStringSubmatch(value, -1)
for _, t := range types {
if len(t) == 1 {
continue
}
switch t[1] {
case "image":
opts.Conditions = append(opts.Conditions, imageCondition)
case "audio", "music":
opts.Conditions = append(opts.Conditions, audioCondition)
case "video":
opts.Conditions = append(opts.Conditions, videoCondition)
default:
opts.Conditions = append(opts.Conditions, extensionCondition(t[1]))
}
}
if len(types) > 0 {
// Remove the fields from the search value.
value = typeRegexp.ReplaceAllString(value, "")
}
// If it's canse insensitive, put everything in lowercase.
if !opts.CaseSensitive {
value = strings.ToLower(value)
}
// Remove the spaces from the search value.
value = strings.TrimSpace(value)
if value == "" {
return opts
}
// if the value starts with " and finishes what that character, we will
// only search for that term
if value[0] == '"' && value[len(value)-1] == '"' {
unique := strings.TrimPrefix(value, "\"")
unique = strings.TrimSuffix(unique, "\"")
opts.Terms = []string{unique}
return opts
}
opts.Terms = strings.Split(value, " ")
return opts
}
// search searches for a file or directory.
func search(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for fb.Errors.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return 0, err
}
defer conn.Close()
var (
value string
search *searchOptions
message []byte
)
// Starts an infinite loop until a valid command is captured.
for {
_, message, err = conn.ReadMessage()
if err != nil {
return http.StatusInternalServerError, err
}
if len(message) != 0 {
value = string(message)
break
}
}
search = parseSearch(value)
scope := strings.TrimPrefix(r.URL.Path, "/")
scope = "/" + scope
scope = c.User.Scope + scope
scope = strings.Replace(scope, "\\", "/", -1)
scope = filepath.Clean(scope)
err = filepath.Walk(scope, func(path string, f os.FileInfo, err error) error {
var (
originalPath string
)
path = strings.TrimPrefix(path, scope)
path = strings.TrimPrefix(path, "/")
path = strings.Replace(path, "\\", "/", -1)
originalPath = path
if !search.CaseSensitive {
path = strings.ToLower(path)
}
// Only execute if there are conditions to meet.
if len(search.Conditions) > 0 {
match := false
for _, t := range search.Conditions {
if t(path) {
match = true
break
}
}
// If doesn't meet the condition, go to the next.
if !match {
return nil
}
}
if len(search.Terms) > 0 {
is := false
// Checks if matches the terms and if it is allowed.
for _, term := range search.Terms {
if is {
break
}
if strings.Contains(path, term) {
if !c.User.Allowed(path) {
return nil
}
is = true
}
}
if !is {
return nil
}
}
if f.IsDir() {
originalPath = originalPath + "/"
}
response, _ := json.Marshal(map[string]interface{}{
"dir": f.IsDir(),
"path": originalPath,
})
return conn.WriteMessage(websocket.TextMessage, response)
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}

View File

@@ -1,192 +0,0 @@
package staticgen
import (
"errors"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
fb "github.com/filebrowser/filebrowser/lib"
"github.com/hacdias/varutils"
)
var (
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
)
// Hugo is the Hugo static website generator.
type Hugo struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Hugo executable path
Exe string `name:"Hugo Executable"`
// Hugo arguments
Args []string `name:"Hugo Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// SettingsPath retrieves the correct settings path.
func (h Hugo) SettingsPath() string {
var frontmatter string
var err error
if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil {
frontmatter = "yaml"
}
if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil {
frontmatter = "json"
}
if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil {
frontmatter = "toml"
}
if frontmatter == "" {
return "/settings"
}
return "/config." + frontmatter
}
// Name is the plugin's name.
func (h Hugo) Name() string {
return "hugo"
}
// Hook is the pre-api handler.
func (h Hugo) Hook(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method.
if r.Method != http.MethodPost {
return 0, nil
}
if c.Router != "resource" {
return 0, nil
}
// We only care about creating new files from archetypes here. So...
if r.Header.Get("Archetype") == "" {
return 0, nil
}
if !c.User.AllowNew {
return http.StatusForbidden, nil
}
filename := filepath.Clean(r.URL.Path)
filename = strings.TrimPrefix(filename, string(filepath.Separator))
archetype := r.Header.Get("archetype")
ext := filepath.Ext(filename)
// If the request isn't for a markdown file, we can't
// handle it.
if ext != ".markdown" && ext != ".md" {
return http.StatusBadRequest, errUnsupportedFileType
}
// Tries to create a new file based on this archetype.
args := []string{"new", filename, "--kind", archetype}
if err := runCommand(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err
}
// Writes the location of the new file to the Header.
w.Header().Set("Location", "/files/content/"+filename)
return http.StatusCreated, nil
}
// Publish publishes a post.
func (h Hugo) Publish(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(c.User.Scope, r.URL.Path)
// We only run undraft command if it is a file.
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
if err := h.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
}
// Regenerates the file
h.run(false)
return 0, nil
}
// Preview handles the preview path.
func (h *Hugo) Preview(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if h.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
h.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := h.Args
args = append(args, "--baseURL", c.RootURL()+"/preview/")
args = append(args, "--buildDrafts")
args = append(args, "--destination", h.previewPath)
// Builds the preview.
if err := runCommand(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r)
return 0, nil
}
func (h Hugo) run(force bool) {
// If the CleanPublic option is enabled, clean it.
if h.CleanPublic {
os.RemoveAll(h.Public)
}
// Prevent running if watching is enabled
if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force {
if len(h.Args) > pos && h.Args[pos+1] != "false" {
return
}
if len(h.Args) == pos+1 {
return
}
}
if err := runCommand(h.Exe, h.Args, h.Root); err != nil {
log.Println(err)
}
}
func (h Hugo) undraft(file string) error {
args := []string{"undraft", file}
if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") {
return err
}
return nil
}
// Setup sets up the plugin.
func (h *Hugo) Setup() error {
var err error
h.Exe, err = exec.LookPath("hugo")
return err
}

View File

@@ -1,125 +0,0 @@
package staticgen
import (
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
fb "github.com/filebrowser/filebrowser/lib"
)
// Jekyll is the Jekyll static website generator.
type Jekyll struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Jekyll executable path
Exe string `name:"Executable"`
// Jekyll arguments
Args []string `name:"Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// Name is the plugin's name.
func (j Jekyll) Name() string {
return "jekyll"
}
// SettingsPath retrieves the correct settings path.
func (j Jekyll) SettingsPath() string {
return "/_config.yml"
}
// Hook is the pre-api handler.
func (j Jekyll) Hook(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
// Publish publishes a post.
func (j Jekyll) Publish(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(c.User.Scope, r.URL.Path)
// We only run undraft command if it is a file.
if err := j.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
// Regenerates the file
j.run()
return 0, nil
}
// Preview handles the preview path.
func (j *Jekyll) Preview(c *fb.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if j.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
j.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := j.Args
args = append(args, "--baseurl", c.RootURL()+"/preview/")
args = append(args, "--drafts")
args = append(args, "--destination", j.previewPath)
// Builds the preview.
if err := runCommand(j.Exe, args, j.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
return 0, nil
}
func (j Jekyll) run() {
// If the CleanPublic option is enabled, clean it.
if j.CleanPublic {
os.RemoveAll(j.Public)
}
if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
log.Println(err)
}
}
func (j Jekyll) undraft(file string) error {
if !strings.Contains(file, "_drafts") {
return nil
}
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
}
// Setup sets up the plugin.
func (j *Jekyll) Setup() error {
var err error
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
return err
}
if len(j.Args) == 0 {
j.Args = []string{"build"}
}
if j.Args[0] != "build" {
j.Args = append([]string{"build"}, j.Args...)
}
return nil
}

View File

@@ -1,19 +0,0 @@
package staticgen
import (
"errors"
"os/exec"
)
// runCommand executes an external command
func runCommand(command string, args []string, path string) error {
cmd := exec.Command(command, args...)
cmd.Dir = path
out, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(out))
}
return nil
}

12
main.go Normal file
View File

@@ -0,0 +1,12 @@
package main
import (
"runtime"
"github.com/filebrowser/filebrowser/v2/cmd"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
cmd.Execute()
}

Some files were not shown because too many files have changed in this diff Show More