Compare commits

...

151 Commits

Author SHA1 Message Date
Henrique Dias
5f986901e9 chore: version 1.4.4
Former-commit-id: 0ab1627b929b5c4be5ebb66b97d7f81fcb480395 [formerly 6a569e0973cb86401d95dcc9285ae81b2b9b49ad] [formerly 571700b6bc7ddc08738f3f887a5cf9dfe127cbda [formerly 93b823e6d1]]
Former-commit-id: a875a5b56484e0d392ddf7a8c60cf022bb46099f [formerly 19a767e19ff83ff34a36b3845d6b8e91063c7167]
Former-commit-id: 4c98b119b38be29e52fc502d635a05491ea9e9a4
2018-01-07 12:40:28 +00:00
Henrique Dias
0b4d8df22d chore: update dependencies
Former-commit-id: 0e9c3b0caff7d2297e06985cf7361607e0e9205c [formerly 397e6c26097c550d8ca74e554237502964b337a2] [formerly 1fe8a4d39b94285a8c5ffb089f6388a38aacb12a [formerly f29e210e6b]]
Former-commit-id: 352a198a53b3589f40ca83148e64149a67f7c2a3 [formerly 93bf553394ea1d30190412fe77db0d456146d177]
Former-commit-id: c07af559899e9d0851ab4ac2573b57bbd8efaddd
2018-01-07 08:20:58 +00:00
Henrique Dias
9619b4ee1d update example
Former-commit-id: 41adc5e8fc05d20caa0d293224c3b987b3d18a9f [formerly 531a461aff8e332598842e2d98355f77ea8caa7c] [formerly 3f84ed6e4bbe7d14e2a1f6221edcb069b2b95dea [formerly b9a0ac4459]]
Former-commit-id: 1eece7e6b527ea1daaba2424399a76e1c702bb43 [formerly dc079172134ddafd696c910f440f4597be1983af]
Former-commit-id: ddb46ff3aae922ecb4818862cad0b8920d6899d0
2018-01-07 08:06:55 +00:00
Henrique Dias
c75f3e0f59 chore: update build and publish scripts
[ci skip]

Former-commit-id: 68fc4cab3e092dbf192f02a7b69cd1024be6095e [formerly af8599c8c136f466f33b3060c1aa0568054617fc] [formerly fcce660652925df3b7837a503114380f6eab269e [formerly 32106b537d]]
Former-commit-id: 42514c2f46abf0ec4533ed44edb2ed16ff1220da [formerly 0bf28005af9a16de599a99d7c0fc543b5208729f]
Former-commit-id: 2f1b4cbd7bce86943ceca8a3f0834a1ac81edb45
2018-01-05 10:15:26 +00:00
Henrique Dias
6d71ab08ce [ci skip] auto: setting untracked version
Former-commit-id: 2fed0803c7febc91e7b5bb3d33d85b7ff560d5d7 [formerly b7153111a10d7bb8374469aa4504b7b63c4f9999] [formerly acd233135fd878ba16bcf73bda9b4358f9a7473e [formerly 1218966226]]
Former-commit-id: e0a33076851b5927794180e4b5bfd1483d51ebfe [formerly e150034ec51997ec1ce075ea28e51a79b60c54d4]
Former-commit-id: 9a52d3ec859ed128606ae01d2bcf6a9ef08cb2b3
2018-01-05 10:10:49 +00:00
Henrique Dias
121c695c1a Version 1.4.3
Former-commit-id: 105f6e3a206963eb810ed178f5a3d05e4c05693f [formerly 95a0eef30242460e822aef2f93ec770f6d82702d] [formerly bb726ba819c7f01330db8dc7503b2f869ace40c0 [formerly 74f6ea6897]]
Former-commit-id: 9db1edc8d09521068b70bc7b81e5d2586ef949a8 [formerly e9e78572d9b296f160b5c311e57822a0e960d35a]
Former-commit-id: 4513e831114a23e30822734d62191a7a49d50555
2018-01-05 10:10:40 +00:00
Henrique Dias
6a1f9bbf69 [ci skip] auto: setting untracked version
Former-commit-id: 500c3e1726327fca7ca8421d70f46eebe76702bf [formerly 09a3811e6a1b87b159fe6ab562f968aef1eeef20] [formerly e85343d82a8de4c9b93f1d5e65e8654e85b67f5b [formerly 13c12b9130]]
Former-commit-id: 26e2c13658759ee83e6d2ec7d851a1cfc2b485c7 [formerly 4eb961b41d768ff23b4d00163f4c0e9512829fbd]
Former-commit-id: d6a362c2611bf9ef6267be25a08e47a8d7f58b1e
2018-01-05 10:02:58 +00:00
Henrique Dias
ab54bc3c57 Version 1.4.3
Former-commit-id: f47b39a9b2817c603578b2623919403e30d3af22 [formerly a8b23dd12fba701c0cbe91049f263318d33a8be9] [formerly 7de9b5f8f6d1c6a962b89c540625b68cd4cbac0a [formerly ac3f1482c6]]
Former-commit-id: 56c305fb8ea1a017daf051ce8f630c2ffddebb7b [formerly bcd1d113a852499dee8701bd64e49df8513f8538]
Former-commit-id: 95ca9218c8b98432f9c5e0019fe619c0dd417e34
2018-01-05 10:02:49 +00:00
Henrique Dias
8a10baa815 Update .goreleaser.yml
Former-commit-id: dce13b52b52d980f11ef90f08b229c99ef74548c [formerly 45b6e868899a14fad61e61b5603da5dbdf1704fc] [formerly 90aabc167231a2d164afee7ab966a465146afe63 [formerly d8f057757e]]
Former-commit-id: 43bae9e293b2e3cf0b8608c344dadff88de47380 [formerly beff2d7f954169e917950549a353f5efa033f906]
Former-commit-id: e77e853b8e818863bff7164b169b411eaa6732fe
2018-01-04 14:04:09 +00:00
Henrique Dias
98c22ece8d [ci skip] auto: setting untracked version
Former-commit-id: 8ddd21218dd4fb6e00cf083a80199d2a4339af2f [formerly 3406ebe12f6112ed751092e5f831c77ce459c6e0] [formerly 143f9d23f5fa5e5545fd63e6f44ae9228e6c093c [formerly 5bd60ce05a]]
Former-commit-id: d7362d24d7d872cb722d462fab45d2a48b37b1d5 [formerly c7c1cfd6d43642315e9a7956211ede7eb0a6b5e3]
Former-commit-id: c42269de8c2b7ecbdacaca0cf383c8a29dfd239b
2018-01-04 13:50:31 +00:00
Henrique Dias
63247f6e8c Version 1.4.2
Former-commit-id: 488d6575d81275f6c5e1c04280a5def6aa700117 [formerly fb198ae1767fc52ed8492657f33f5bd91d4d4283] [formerly b100a438db05a237c2e7a081fbdaed3a5ed6ebae [formerly e7b372f548]]
Former-commit-id: 3c418fc25a996e829e35be8f49eec0c43bf1829d [formerly 6568a1dd89e6a5640a2ad9fde224303813f219d0]
Former-commit-id: a1355be166395d2bc0d0a4a5a7170b36c73df094
2018-01-04 13:50:19 +00:00
Henrique Dias
58dd812008 fix #269
Former-commit-id: a0b8ed91061292e0f155cee9a7865609daacf836 [formerly e6bb9eaa82b5704f3fe29bdec50f6bb4f55304b2] [formerly b443cbf5c6d7946ff3cfa44493449b5f0d75d830 [formerly 02df167434]]
Former-commit-id: c193caff6faaf78a16375960893be9e253869d3c [formerly 6c84d234c9e1db432027bc159dbeeb77e9c35031]
Former-commit-id: a48c3851511472efc47bce07ee2509fdd8a44c4a
2018-01-04 13:49:41 +00:00
Henrique Dias
1575fafec8 [ci skip] auto: setting untracked version
Former-commit-id: c497913d120be4a71f7d2305600e2785190f0e2b [formerly 0126f289afd78bc006e5e0fe383670dbc9df4fd4] [formerly 12899a40ac7b8a94c3930dcdb3101d121c56cbc2 [formerly 9da06a7010]]
Former-commit-id: 866590120ccd5ddc189d36e352c637cabc82f81f [formerly af0b154bf6db52c01e9d0b10f3d6678319727bca]
Former-commit-id: ff3c428b12cc8028b21d8585a9927e826ebdf4cb
2018-01-04 12:41:34 +00:00
Henrique Dias
8c935eb51d Version 1.4.1
Former-commit-id: 77ee0108106777f2de0719eb6ea50bcad12f8635 [formerly 4dc3acf5b7eaaa687ccfced7fc463e2b7e7104ea] [formerly 3ec2bd1fe0b8e364621204b9aa1732cbc7828199 [formerly 44d6730073]]
Former-commit-id: efd15158f953966190c8a8e9310801a89049ea4e [formerly 56a49a89f919d70486b6db126e84e5997bd6d261]
Former-commit-id: 969b87ac24214cb7dfc9429253724c68aa0d46bc
2018-01-04 12:41:23 +00:00
Henrique Dias
7f3a26e1c0 fix: error when user have no commands; close #293
Former-commit-id: 9e1ebf686f12d76a202bf8a3b0b16f14f8eb713d [formerly bfc761cc181d6d8880fd86396e69bf5894806f6c] [formerly 774e09b49a2dff07becff48bf048b3b0bd8353ed [formerly 65d3c0fb9b]]
Former-commit-id: 8ab97c0103cde1a9bcf69c1e9af994241fb9d124 [formerly a25dc7feb0755122c7329e3a3bfa61f74418467b]
Former-commit-id: 92e21139ffe04ab23406352e0a5b6131e8cad084
2018-01-04 12:40:45 +00:00
Henrique Dias
fe834aa244 remove file
Former-commit-id: 95f665f00dc03c130afb5fc45e8076a869c30222 [formerly acae04da4a0d58e689b704ff405a749c4fa43c05] [formerly 482666a8335cc216fa706d5175c8a75cbdb073df [formerly 0781b961da]]
Former-commit-id: 57a2f201668aad631fead876207b2d5620e202dc [formerly 9f1344480aa6589652ce34dff2496dc36b9d631c]
Former-commit-id: 2760b12b251f53c4421b80d5d5122520eb92cf40
2017-12-29 17:31:21 +00:00
Henrique Dias
6620a1bf41 [ci skip] auto: setting untracked version
Former-commit-id: 0b2303648bf1c82d04b20f439a76d2ce37cfbc4c [formerly 8aedb0827d0538efa6ec0d88082e3df2e85b6e46] [formerly 91dc35e45674e83e44ecb66e7da2bb1d7634efb4 [formerly d431f5f0e4]]
Former-commit-id: 551cf4271e33f7893ecbd7290074d0d031827a94 [formerly 034d533f73603df46d2c7efccaddd17bb0f37814]
Former-commit-id: 1cf3cbcb484e5128ac9564516992f5fbdaddfc28
2017-12-29 16:46:22 +00:00
Henrique Dias
dab08325b7 Version 1.4.0
Former-commit-id: 368d4ecf114fb68ca038e4baf3d840afb2636ba0 [formerly 42d7e7c749edda5dc525f86e775c999b69dd0343] [formerly edb264480cef71783fa0ec799c667710b48c0e76 [formerly 0848d5b135]]
Former-commit-id: db6356cdcaf40265c31e4aa08c88614d2aa1f886 [formerly a52ba320deffe84af53facdba3606b9d032118ea]
Former-commit-id: 6ee806954d2456421fe737541dfee136df93f54e
2017-12-29 16:46:12 +00:00
Henrique Dias
ddb4685257 Update .goreleaser.yml
Former-commit-id: 66c518285185e0a94bd1ab865676e3e5f409b76f [formerly 906313ef1a1747ce137bcbe7d54a20244b04ced9] [formerly 4724da443d609b6ef27851c2b79eadb9d7cefa4f [formerly 2fc342a3e9]]
Former-commit-id: 90b7247bd50078d5321c9dc1c51149f88c63989e [formerly ba9587d799234e3cd1c2888841fa6fc12227806c]
Former-commit-id: 9b9383f2cbb64775944dc68d882454d73b406246
2017-12-29 16:45:21 +00:00
Henrique Dias
39dbca3b15 [ci skip] auto: setting untracked version
Former-commit-id: ea04ce9843fafcc33961d3aacd5f31b5f93d1a08 [formerly d97fa13252b9201ea17bb9aa188b7f93b03d3f6a] [formerly c647d2080414bccf957ffb713cf029219780f606 [formerly 2b9cf83feb]]
Former-commit-id: 82f4a48d20d439257a70e49726bc26ddbd259c58 [formerly 8d3787cabd875c3ee84044051e5f97fbf1922585]
Former-commit-id: 45a6d0ca105b6b9591928d69db81c7c7777b62df
2017-12-29 16:35:29 +00:00
Henrique Dias
a02bdfc8d5 Version 1.4.0
Former-commit-id: 3a8e072916c7ce76104d67e805c202cb95568c41 [formerly 6c77d93905851193699f1651653de748fc8f0806] [formerly 8563da03483dfdc77cb65736deb60084ae76df40 [formerly d98b58e0f0]]
Former-commit-id: 745caf9deb45ed766f088d7a52aa992191960a40 [formerly d2e6c7c774dc6b58b9406081358730f4dc5c573d]
Former-commit-id: 37ba109c46b21bdff083ee63451cb54bc3243e3e
2017-12-29 16:33:50 +00:00
Henrique Dias
f88721cc83 chore: add more build platforms
Former-commit-id: d3442dce2a31d700cc0778229819ee3821bdcc19 [formerly 218c9d7fb7fa28546797c50d03956362829407a2] [formerly 6abbb1b814a037c106526506ee1ca999dae54914 [formerly 3217751cc3]]
Former-commit-id: 4fb0fecd3b19b145369d751f454b5c079163c9f4 [formerly c428f4e11ce84ca60ed7c2d968da9c71c1f23d11]
Former-commit-id: f22d06e1d43a9eb68845aa2490c72b187eba0018
2017-12-29 16:32:32 +00:00
Henrique Dias
95544bfed5 fix default view mode on caddy plugins
Former-commit-id: 41e6e855b611449829b4aaab2381685da8b94bb0 [formerly cfcd8802666a8869a2e2f8229d86fee05e088d4e] [formerly 72b0f44e1bf1ab0b27cf327cfac6977049e2dacf [formerly 258557fb57]]
Former-commit-id: e17e5ed75665cd53ec8a20b2833268de5ffa5e47 [formerly 5b8571fd62e0517f1daa72fbb95d7c99ba120c6b]
Former-commit-id: 39ebfa5155c60f71e9f565de875594c3b07b8bac
2017-12-29 08:55:03 +00:00
Henrique Dias
999ee1c2f5 [ci skip] auto: setting untracked version
Former-commit-id: c555833bd28140ffed26dd56d5d1f03ccdefe8f9 [formerly 19be792ade9e4df3c4cc0765d819be35269803b9] [formerly 190d7e277e3d94b22af59288b0403c1e6055ca58 [formerly dc1f51cfa9]]
Former-commit-id: d270e36617e94f506828700bab84310aa6199750 [formerly c414ab0a208bc6fa5a4236bd269d42c2fab3ca8c]
Former-commit-id: a82ebba0e51012db7689b41f81c6cbcfdac9accb
2017-12-27 16:20:03 +00:00
Henrique Dias
a61ade62e5 Version 1.3.12
Former-commit-id: cebe84ae6a9b87fec49fd1b2966f58174f49b616 [formerly d8fc91407b0a3cc7616532f14e5ae9b8af49ca11] [formerly d52eac19847da77e49817878f2930c8f438af49a [formerly cec257787b]]
Former-commit-id: c8913636186c289c615ee29d0b36998484d39023 [formerly 67b4f850b4452112f63f2b10e6ae9f9e8848d26b]
Former-commit-id: 45593be374fb4c925a93c26e7ee1288e53c3abf4
2017-12-27 16:19:51 +00:00
Henrique Dias
08de5efeb4 fix: stream file instead of putting it all in memory (#303)
Former-commit-id: 2dc1092f4111870eda6fd51f06cf6dadcac27c78 [formerly 6b21e8ca91b69a70aa07a60099e6919cf1f29fff] [formerly 5860c903ca50aa9a29f91c98d144e859fa223977 [formerly e6a8e3349e]]
Former-commit-id: f8923f5d3fa5d638170ccd870ed4d38abee42d2e [formerly 1a1b27bb606ea012bb5eb3ee42ab6f1d585e1490]
Former-commit-id: f7a238d6ed3e61b925fa20e7ecca145b1f903dba
2017-12-27 16:18:27 +00:00
Henrique Dias
74f690a71b fix: allow user not to write .md or .markdown when creating file based on archetype #288
Former-commit-id: e2bc376cc78905e9d85596bcb281b1f52647b4fb [formerly d4c44f8625bda57d5659366d2651d8021776c9d2] [formerly 766fc98ad54327498076992a110415750d059bd2 [formerly 390fe1d797]]
Former-commit-id: 6c30496fab170a7a39eacba39827e054089f64ad [formerly 8b3aac4b83339ec9722ddadc3fe225e3753afd94]
Former-commit-id: 85e39aa259af1e628e77d488fbd9c7ea5d207eb7
2017-12-27 16:02:36 +00:00
Henrique Dias
1d4a3005ff fix: hugo full path in URL #288 (#306)
Former-commit-id: 56143acfb6b2817df49998c96f9b70174327c31c [formerly b763216262fd57a06d7f343a96891cc62f5ba358] [formerly 8a09d84e73ce9e3865bcd65a8071f2e5cb48333f [formerly 68c4072bd1]]
Former-commit-id: cbec5adf02b6d83b27c7286546a7a291b2344256 [formerly 94dbfbb4aaf51abcd809f3562a4a8a53945782f5]
Former-commit-id: 3f11f9c8be4f7e9b063789fca73212ca838ce1ad
2017-12-27 16:00:16 +00:00
Henrique Dias
7447a530ee Close #289
Former-commit-id: 6145bac21a69ec5af8b0c9d6f5a343ab66c32c74 [formerly 13a045c4cc6852e88af60b359653032590c8cb48] [formerly 1b279454432546ba44702b792ec83eec00f63a1f [formerly b1c36ec71f]]
Former-commit-id: ef359bdf00c3c64d4877f02de3d8cb505bd6448c [formerly b4f09c805cd6a0f5300a1e608d024ce7af6b60db]
Former-commit-id: 009fb85389f234e1d5f7fc726e1d983fed3b5832
2017-12-06 22:30:49 +00:00
Henrique Dias
fa9396f0f4 [ci skip] auto: setting untracked version
Former-commit-id: b508f843299648b61849e33273041df46c2ed31e [formerly 9e153dd2ff31f0bb769e35f70c2bdba656bf449f] [formerly 864e40ed1299bf717242ddc1556d3f3ccbfc2f89 [formerly 6856716adb]]
Former-commit-id: 88b556096518c23d2db8a26c9c448400172d30b7 [formerly 9b9d9539c8ebf8c4ff5a2288ca04ade387a84351]
Former-commit-id: 625b4ab5af200087a85db6e1402c9bc4a0295d83
2017-12-04 16:10:27 +00:00
Henrique Dias
172bbb1828 Version 1.3.11
Former-commit-id: 4a93c06ece303164400a26def8508b415ce8c906 [formerly cb7e56f73da93ce59e63b74de61a384d49335991] [formerly ee5c57615221b3b91660df2146845b413c972975 [formerly 506570530f]]
Former-commit-id: 3b120ed2a0b36e84dc258862663421240866601a [formerly 641b5b688bb500b711cc4b14f4eec48c10c23d6c]
Former-commit-id: 965b1316ab20445fda0d3b64c45229c98496534f
2017-12-04 16:10:15 +00:00
Henrique Dias
410aa5d9da Set correct indentation
Former-commit-id: 0088ec263439da995dd6f42d0e2ad9b0b42c7c98 [formerly c5232aa97864225d432f04d5d83e0215edff18c9] [formerly d038a6f63e089003de83bbb054d76d3953cab33b [formerly 0dac28fbb0]]
Former-commit-id: 44d9d716ddd2eed8fcfa7bcb92f5f46475ab0afc [formerly 9f9d3a6c0bec92f75b6ad6932e6b1eebb9ddefdf]
Former-commit-id: 9e2919024770313c9ed94c1bc9060f2e45871f77
2017-12-04 16:08:24 +00:00
Iñigo
736cef7127 Add Spanish translation (#291)
* Add Spanish

* Add Spanish


Former-commit-id: 52479d923ca4a91d3cd8b627246de8b70c3a5152 [formerly 53bb5278073dd5abdd96d04b9423811ae78637e9] [formerly fe82800194e49c99d278413e79f8c007bf4dfab8 [formerly 78c68ce2f3]]
Former-commit-id: e1314f8d7a5ae78cfbd3727339016d6b322c4aa5 [formerly a0a6d84f266f512c5f9eaa9c92d0f7b57f447367]
Former-commit-id: 6a557d9ab0e82332b201534276e1c032e682714e
2017-12-04 15:49:56 +00:00
Henrique Dias
f4ceb7163e Update file.go
Former-commit-id: 713c809c9314b3db947fd33e5fae69bbe0058865 [formerly 4dd6de8cddb93b1553a6d768db426ec3eb1f0b53] [formerly c5033b6ff486b786afd6b75e41ff35c3a8c69e47 [formerly a34422de57]]
Former-commit-id: 1028ebd6474ce1e257ef1f81c9f805c74f64994e [formerly 5f2db5f3060c457ff2af82e0e0be89a25d1cbc4d]
Former-commit-id: 609be343302e0a49190ef6f37ee677116e9baa04
2017-11-30 18:22:16 +00:00
Henrique Dias
7bd4fbc0cb Add Badge and Update Deps
Former-commit-id: 8d3cf568a2f901ec8370a5f7aaaf0aae95425353 [formerly af950ac5e3ddc57c599b62937ed8872bb653f426] [formerly 3b9e1a7c8d9ea35b10eec9a82fad1221830ae5b6 [formerly 1f62c26e81]]
Former-commit-id: 31e0e2de46b44cb85f4423be7064f4d6d9552ef9 [formerly 431ed7d219cfec8b98fbe4423ee66c23e7849b74]
Former-commit-id: a851d7f44f290616d042542edbcadf35f856d73a
2017-11-21 19:17:19 +00:00
Henrique Dias
2ed4658369 Update assets
Former-commit-id: 921770105b3b607f2681112d204130a0655248b9 [formerly cb7d152432980c20f5d5cda743992b6a0684ed0e] [formerly e63e514df0f701934dad6744d79c7b7746c372e8 [formerly ec30606dcc]]
Former-commit-id: 725b6820ac82bfab769306f1fc92acb3f690f366 [formerly 56b2b925d513261885fbdc7cc0d3662f9576d56a]
Former-commit-id: aeaeb164f0c2c7579778ae1bd587806b69868c3c
2017-11-19 11:22:54 +00:00
Henrique Dias
735312982c Update Dependencies
Former-commit-id: 76bedea1c27cf4e87cb9255786de3d73e6abd29e [formerly 8c21d31b2c5a22d96f3a7a9042dcd58077d7dcef] [formerly b335f0ac2e16fdc8042255ef1d0dbb421c4b09d4 [formerly 02b2bb6215]]
Former-commit-id: dbc8cfd8755959d7d7f40976c2641969e08cdb5a [formerly 29b617fd9461a72b0d0ab729a6a1d7247c5ec74e]
Former-commit-id: 23a7b57755f5fea3e0d62450e6466267227fb865
2017-11-19 11:05:05 +00:00
Henrique Dias
4ddb3f5a34 Improve file extension checking performance
Former-commit-id: 30aaff2f0bee9dd9743d4acd6a68aafc90addef9 [formerly 967fc0b47a5fb0d1b5e79a8b926f4fdd61b8477a] [formerly 5eaecd069cd3de9ebb5e3e6d0feb04844fc473c3 [formerly c76c69c256]]
Former-commit-id: 11d0c1ae276859eccb7949d96dedbd0cd3204ffd [formerly 63de350fea7eb0a6d621b029c9c8866866462d31]
Former-commit-id: 422edd1298a7ba86dd958dcc7a513a0a3e6d83a3
2017-11-15 21:03:50 +00:00
Henrique Dias
c947228ac4 Whitelist yml and tml extensions
Former-commit-id: aed7606c12fa705954e650ba948d891329d66dff [formerly efd4415075000737c5dc7bb5a38a78605989e05c] [formerly 5656bb7bc09285720f094b3054c70994b51aa658 [formerly 8f5eaee952]]
Former-commit-id: 9d774b10c85ae9456a324030f9b7b0c06e600db4 [formerly 9c7919b5e027d829364fcb658b6b9579caad51a3]
Former-commit-id: 3196b0ea7f550057e411410e083a8c4f8e748240
2017-11-15 20:38:30 +00:00
Henrique Dias
505af7d9d7 Update auth.go
Former-commit-id: e2a955c377393341ac3ccf08d2fba9e3359e72b0 [formerly aaf783e1c2aec83c5e0a4a016f98b9b17ece09c2] [formerly a3be55338e9d4403ae3266cba5ac2451aff507fc [formerly 1f946bd94e]]
Former-commit-id: 53e720dcdc8745aa5de632a5d2676ea1c0648445 [formerly 7069f0e163c34e1890d344066ba570dde674a414]
Former-commit-id: 2aee9f0b0ba181bde240bdcf098f3914c3f512b0
2017-11-15 09:09:09 +00:00
Henrique Dias
2c13ac4ac1 Remove fmt.println from code
Former-commit-id: 6bdb55590fb58a6edc64a3ca946a13ee35f3ad60 [formerly 60939e561228d83df8c588942f6773e5cb508fc6] [formerly 0764f9f8de88d1adbb668c48dfee17c43d59b5f4 [formerly d3eafec864]]
Former-commit-id: c09a0c84177f6f8053269cf6396b64cc17bf257c [formerly fc5b21e08ca0f2ebab30991a4c7f2ee0eb0d6a40]
Former-commit-id: cb8338d463ec7014de5e2cb09009ac872ade06b8
2017-11-15 08:59:23 +00:00
Henrique Dias
b5fbab2072 [ci skip] auto: setting untracked version
Former-commit-id: a15ba929646b6a49aad1c0a1f447c8417490bd82 [formerly 2140e8ebdbe698e68fa7cb80c879749af21a67ac] [formerly 8c2f7dc49612073e633affb68f437b8c2241b609 [formerly 927a1aea6c]]
Former-commit-id: a1daf8141f756ad4352bd26697d073c2468b1163 [formerly da39c59b7c0187c61b80fa4fb4867795f2b60b17]
Former-commit-id: 7b1fc0b045cd1e1d59fb49bb9b6db819e670fd94
2017-11-14 07:25:09 +00:00
Henrique Dias
005b184d59 Version 1.3.10
Former-commit-id: f9c2dd8d2b77b8d5e887562032777a108cc75c31 [formerly 659e0be49e23fed2271489b14db6fc51a25b7e0d] [formerly 57d196da7a6f46d3accb40ed471adbed67ebe5bf [formerly 0c657c3019]]
Former-commit-id: 40ae9fb71bcc1989f00c20cf05b54ebf3db3f0ff [formerly a51a873c5262d30736274109f7d779e5cc45e423]
Former-commit-id: 45946d34eca1ae4198ea113bc886d43f12322a65
2017-11-14 07:24:57 +00:00
Henrique Dias
372bd813d9 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 7b068c22b37003acf5dd60806a828ed9f828fc95 [formerly 10918882c8f3150b57a5cd142e7443150caff282] [formerly be0e1f9cd900feead555280dbff11e024055cd7f [formerly b8fdb487d9]]
Former-commit-id: cf6f5b27bb327bfb00d01e249f8db457543407f6 [formerly e5e0ceee98ceadd4061f2443b6c4a53249e838a4]
Former-commit-id: 2187a74e21e29e8cf5b13bcca6724ced2aae47c9
2017-11-14 07:23:08 +00:00
OopsMonk
52599314b0 fix symbolic links issue on Linux. (#281)
Former-commit-id: 84a15d1ff70cbdcb91aaa7a40cb86604f036e349 [formerly 60e5a702fd98686193c5ee7bfb66698453de05ae] [formerly b863bd64cfc4e953c005f71ccc3fd936456b8a5e [formerly d269e239bf]]
Former-commit-id: 24b6c15a5c3a54a866106eff843bea0d7401cf15 [formerly 6b6c3036a90776bb16d93a4b7e864e4f433f5531]
Former-commit-id: 5b7aacabf2ceb1a5303fd303d5132bbdd711362f
2017-11-14 07:21:26 +00:00
Henrique Dias
f61e71e44f Update Dockerfile
Former-commit-id: 657785108f5de159eebb842d402ef98d512f96f7 [formerly addc47263d93e2895204151aab13539307533430] [formerly 91081acc00772f9fba15eec3362d94cbf9513145 [formerly 6f39464130]]
Former-commit-id: 54dc3af451a3bb3e1fdd114b88e0dac7c1a8802e [formerly 49d5afc124292884245cacdd951c5f9670cb552a]
Former-commit-id: f331a53412d10708e0ac19db04d94f0b99e63b40
2017-11-11 07:08:12 +00:00
Henrique Dias
e5265b6632 Update Dockerfile
Former-commit-id: 2b02497070be694a121f6ae8f7567ee521d2039f [formerly 6c301dfe46f6e1ebb9d92fa5251c23aa6a5c9f64] [formerly befd05696772a79c19330a4d3f0b4651c35802a4 [formerly 44d68a4868]]
Former-commit-id: 9f8661bb18ac3230d4f65fc4c5c8ccf31122daf6 [formerly 683bb4fdbfc8c95f1e69382322a8f1132c3de457]
Former-commit-id: a7af592420975962f5556ba47801f9cd4f009fcc
2017-11-11 07:06:23 +00:00
Henrique Dias
003f361956 Add /tmp folder #274
Former-commit-id: 938bde70202335858fe569949b65c04e7ffc4146 [formerly 37c4c3ac2743f7be8e4a8eec92565e8c3b4303e2] [formerly 74b276e4616904471b27d3478b3a58c2ba09fe42 [formerly 00da35b91d]]
Former-commit-id: 7be322ae95ea19b352cb4799b8927353b69286f5 [formerly c662fbd6eb0dc63624daae038897bcede3f28ed7]
Former-commit-id: c8ef89c3ea2239cdf715ad9f1555c1f28167e4f2
2017-11-10 22:29:15 +00:00
Davor Kapsa
cc8369d83e travis: update go version (#276)
Former-commit-id: cb189a867e029cd6dd57e6abcd70f74949e770a7 [formerly 92b3401b09d16673356c16321a1fd2938a8002fe] [formerly 5118e5894f9dec2ae5ae337e300c033f763be0fe [formerly 0be294a502]]
Former-commit-id: 516ccab1cfd5c713eb79a5ab4907e13d1bda98e2 [formerly 586b5951af545993c9ee4bff07625bfc26a3e439]
Former-commit-id: 77e2897c8571704617bd721e46a50a46f6f9acd9
2017-11-09 20:58:34 +00:00
Henrique Dias
aeb583a0bc Improve readability.
Former-commit-id: 684d5ef09624509fb041e47539dc0a568d1e8ac8 [formerly 3d0b8b2e61c5a60174a1fd8b06f8d1e8a55120d5] [formerly fc6cb1f278dbaa90d7db1c4c96cb3c1cbff06c5f [formerly ac88aab3eb]]
Former-commit-id: 80493535e416cad297aed47e8a8dd018d2fd2346 [formerly 3522c56e45bc8c0e2c89177f96c7f812f6c9a6ef]
Former-commit-id: 3096eb9e8abeee11f2ea447aeacfa681464a1252
2017-11-04 09:23:55 +00:00
pandarong
41f07c64eb fix npm run build error (#272)
Former-commit-id: bb2a5ea8b5949e11629d344c319e4cb0c5d48f0e [formerly c6b89a2ab4c66a1e9f16116906dead677e03a5ec] [formerly eb2a1b5b8840883ee18af987f6b7cf16a9d70075 [formerly 8703094109]]
Former-commit-id: 4208e88d8263785a5cc5bacd1127ce8460df960a [formerly 7d45d39e5913b04ca12998ce622188f3c4b975f9]
Former-commit-id: 6020a51629bbdd71ddc131c8ab9c6b0db5eb0f56
2017-11-02 21:21:16 +00:00
Henrique Dias
98588a66a7 Remove unused commentary
Former-commit-id: 6c214b9e0e640036cb012e1000c6a7506a48fa5f [formerly ba4b53ecb41176513699bc883be694b1be3d586b] [formerly bf971d7279fcdb0a734f0f515edd02c405ad86bc [formerly 45a6fd563f]]
Former-commit-id: c50f55e67204964a33fd324b4c948b164420e1df [formerly c00391a35a2aa77a4f5ca2aa5325ac763da263be]
Former-commit-id: 4548263d313aae263f97d2786112a65dd7e056c2
2017-11-02 09:57:47 +00:00
Henrique Dias
49f32f876d [ci skip] auto: setting untracked version
Former-commit-id: ab889938f319abc51aa1eab32ba12d4773b82e23 [formerly 56bf07aeec065255b34e9de3024a1116abd90e1d] [formerly 51aa29d909009fca0b83c71e8bf106ae403091b0 [formerly 2ebc7708a7]]
Former-commit-id: 22662250fd4e0e82ab7dda0f201823c68d437398 [formerly 7c4865e1c894d538d0627413454145d33df6b57e]
Former-commit-id: b3313117d95195075241fd082380de05a582f958
2017-10-31 19:20:50 +00:00
Henrique Dias
aeb0f37e20 Version 1.3.9
Former-commit-id: 9c7d15644bcab9e3babc8d645615e50d99187871 [formerly f0a0bf08c328b801cc6ed5add830732c99f92047] [formerly 250bc983264deb841b2115a03610b8e6f473f1ca [formerly b903356b94]]
Former-commit-id: 56bb31f4e44ad39a65f1d1eaa0a8e493287fad7f [formerly e120e69e05cc67f377a83f19f4106b7be945780f]
Former-commit-id: 36fb7783992200295b2f9811b97c2312977023a4
2017-10-31 19:20:36 +00:00
Henrique Dias
59a0daa293 Fix ViewMode related bugs:
- The user will no longer lost their 'ViewMode' option after being updated in the settings.
- The console will not output errors due tot he scroll function when Mosaic mode is on.


Former-commit-id: 97aa6abdc8b864dc7a55dbf03a2e58895ea7613f [formerly ff9e6ff0898f32bd106b644b2e9002b5de45281c] [formerly 556cc12bd5ff1d91776c81f48dd1dceb8bb627b4 [formerly 51104c5ee7]]
Former-commit-id: dd63b2b818a7bd4960a7243866d6b2829f4c03a8 [formerly 45855d70eaa9a2b060d3a89cb70388040ea8e6d9]
Former-commit-id: 6e0ebf10d7fe3e2c234abc9c0ffd447cfbdd7455
2017-10-31 18:23:31 +00:00
Henrique Dias
c716d126eb [ci skip] auto: setting untracked version
Former-commit-id: 88d8cc322b27fc1165a53779e7226db8fd6f90ab [formerly 7f0e5a193dba73cb133bd4d72cd0d81bdc354736] [formerly da79e8c40409e5b5995749556155cccd8a0325cd [formerly cc5f3ac1dd]]
Former-commit-id: 47f0176b53e3a5b2eb402eba29a07f51b7d0c7a7 [formerly c1d2d78eb8827f3386080b0309e6cef66bfed9b1]
Former-commit-id: 64c1ec091ce58eb6b38b0281b7a728b2ca07fe1a
2017-10-30 16:24:48 +00:00
Henrique Dias
5dc210c000 Version 1.3.8
Former-commit-id: f69ca91fc441142e07dfe8dd5cb92c419a1cd44a [formerly 679e456d16586b10e00c5aceb1cea75eb62e10dc] [formerly 50f4889e1eb2797c0c899af48acb1305fa33b671 [formerly 1e700ef167]]
Former-commit-id: ba0df3d330275d2fa1a5e02c608051f3f1422124 [formerly 77d49e18d479776cf670eb008878364fd4c7292a]
Former-commit-id: 2bfe39e4b6371996559557eeb00a15a895e4321a
2017-10-30 16:24:27 +00:00
Henrique Dias
3bb3fdb956 GOFMT
Former-commit-id: bbc11e551ed79fc41af61d67bfb9a882dfc8e655 [formerly 4ef42ee3251bdf613b7e24b50692629fbf8965ed] [formerly 10abcd7f0e34753069a80a683df94084f03fdb4b [formerly 1d4cbd622c]]
Former-commit-id: 7ac0c18ad61cd491f22b906e1efd87892d729cb2 [formerly b45c7e312058490ae90c8162e6cc1264613a9126]
Former-commit-id: 1c7e8ebd2f1b1dc5bd25df9f30a77c9b73ccad4d
2017-10-30 15:47:59 +00:00
Henrique Dias
b96a4a63c0 whitelist .htaccess to be edited; close #258
Former-commit-id: 1114b97a9b0cf236d96ce283bbf8d482cc85170f [formerly 6419fbd11f28ce1dc5ebdfcde0d34adabd016052] [formerly c1135ecdb720db11f4e1182c5456c484f5f58ed1 [formerly 72ee240ce3]]
Former-commit-id: 09fc62ffc432b822ea495b19bc5daafbd3ed8912 [formerly c1eb12242b30a1d90299427c387e0b2a25ce0f24]
Former-commit-id: e097526998a0d9c6382bd9d97af3bf161e8fcd0c
2017-10-30 15:36:40 +00:00
Henrique Dias
3d54b2bd90 fix #260
Former-commit-id: 0d8742754bb756ad3a83599850dae5f477282430 [formerly 5cb7d75b695d8400fc2af87edd551d6450e7365f] [formerly a6a814c40a5ff4f195c4ab470d4fccc92bd8c1c8 [formerly 99c8c92c6c]]
Former-commit-id: 45eba5ff05f8e64fbf33d9d670e19a0cf4880656 [formerly 88dc856045b9d51596f36ce387b1c4f3e85a7d3c]
Former-commit-id: 1eadaef460060da8ae71df3c66f242c844992725
2017-10-30 15:24:06 +00:00
Henrique Dias
95d43a344c [ci skip] auto: setting untracked version
Former-commit-id: 4f9b713ad6cd1f90e7569c9eb083b2740b2f6a60 [formerly ffa130fe00c78b2431ceb16bf1e18345a98826a3] [formerly 5aecd62f16317a53810677fc49a40af297d6bd91 [formerly f4f7147717]]
Former-commit-id: cd5e3739e28213efeaa59cb1fe68b25b35068d46 [formerly 10edbd82feb4d35296c1edf1ab977860b7504470]
Former-commit-id: 4067d711f14eb26232204a89a6263b989ee0494f
2017-10-29 13:33:42 +00:00
Henrique Dias
1077d5cd6b Version 1.3.7
Former-commit-id: c76a13cb8d0bf0a360c63c18cbfac378f8888425 [formerly e58788aa3de9e8fb54894a72bdd567c3846bf943] [formerly 77cd4f2feb8ef317cfeeff84d85ffb7eff4d899f [formerly 3cddb26572]]
Former-commit-id: af62db2d663287f662990539be428ae84f1974cf [formerly eaae8679ea0b9daeeba5cd29dd981d60d66e6c1e]
Former-commit-id: 886b61f0af26b85625cf7092ce5e7d4c5e44ed59
2017-10-29 13:33:31 +00:00
Henrique Dias
57cc174b3d [ci skip] auto: setting untracked version
Former-commit-id: ed15673452af4cd3ccf1a1f316efd6a733e6bd43 [formerly 0a9637c464bbae4c5c351f7e6f65dd24197a9968] [formerly e7388628831533d4476ba69c8d0cfac3ae18be14 [formerly 08bfeea103]]
Former-commit-id: 412e1b3bfcdef0037292b7ab3f5a5eeb4e9523f3 [formerly 2ca352a5af95eedadc91ea207833bdb670f1481a]
Former-commit-id: b359f15e105d55a9761aa9bf43c331bad113d561
2017-10-29 13:32:10 +00:00
Henrique Dias
cf61baa273 Version 1.3.4
Former-commit-id: 6d70d81a1967f171a41cecbdad8664fc65d1111a [formerly e273e78de0030dcb2eec730a00f248f1e8dbd9f7] [formerly 37b0ede2385e153153a854112ffab1065bcdd44d [formerly f89b86939e]]
Former-commit-id: 84fc2f9742c2e0011254ee762faf6f087986c6fa [formerly f22c175e96173bf25aa08067533937041152fdb3]
Former-commit-id: 32981c2a1211608d2d5c059264aa5eb434099400
2017-10-29 13:32:02 +00:00
Henrique Dias
2d5cd2d1d3 Fix Travis
Former-commit-id: 435a3bebf2ac1bbae2f1ed94d596956227141358 [formerly 46e69c9783cb07a9ca6b7bd428a1ab54cff2005b] [formerly 36ed5ea293a4d39d161bffed5b18a1d7f1a0d5c1 [formerly 3434090a7c]]
Former-commit-id: d5bc8f96b631af8d440438c1a089e710ae09a47a [formerly f447c0c7031af05129bf7c45b6c87168a365738d]
Former-commit-id: 627ac93b54b236e20d552486d125cef813eaf9e5
2017-10-29 13:12:54 +00:00
Henrique Dias
9472aad877 Fix #262 #266
Former-commit-id: 0acca700ed0a2ced8bf6b5b019aa6ff4774b8ea8 [formerly 0ff934dd107e56f4b7349f1363dfb1b6cbbc7916] [formerly f8b60e60087d754b46ca4dad49a151bec299febd [formerly ae1702c4e2]]
Former-commit-id: 824abe0c6cb5d32c061e8efd4a7f1d3354b3dbfe [formerly 655809d5c670c9de352c473de1fe08303a4c5506]
Former-commit-id: 6ca6f383602241b8a50c13cce5b066cd5f2cf990
2017-10-29 13:07:48 +00:00
Henrique Dias
dd0f3ef144 [ci skip] auto: setting untracked version
Former-commit-id: 7abc56816c4aee81b91bccce14251ed71b02ac0e [formerly 5d1acb37bf487778cb740299a0f25846f49f0ad4] [formerly b97420d631839e9a7a74961403113986d493638f [formerly 26e1e44d60]]
Former-commit-id: c196dfa47127e87efcec64c4cda2e7013e5cdc4d [formerly 5647ffd57b1e049a3e11f6ec37fd2344abfc591a]
Former-commit-id: 3cc55fc8d38f553f0405972dd39da2b0bbb52740
2017-10-15 08:28:41 +01:00
Henrique Dias
44e492160b Version 1.3.6
Former-commit-id: 28c72f8ee9f7cf548dd5f2c2cc564b31ba13ada8 [formerly cd087b92826cb418d4d17ac26a8cfeb408a54919] [formerly bf241b663bdcfc00737f5bb429ef4a669c1a6c2b [formerly 3e9c3ed912]]
Former-commit-id: 2b74d089de06c4fa5d4933e0c11ff8b1afcd2f46 [formerly 9ac185e1ea76c1beec895f6c51109fd137547e60]
Former-commit-id: a27b4e08dc726769b4731abb5865d2e908387774
2017-10-15 08:28:28 +01:00
Henrique Dias
0137b03887 Fix Portuguese language reference and DRY
Former-commit-id: 3954a92011b3c1699236b32373688ce80f720179 [formerly 67bca685bac4e893f836b21f85f4369444f3e708] [formerly 9817c40128b6660be30ea48b5317d07d21b84ca0 [formerly 77df2034f5]]
Former-commit-id: 2681c1f068e8094f0f9593a4c21fc6f84d8a6353 [formerly 26986c2af1193a31e3abec64b829cb29aa97011c]
Former-commit-id: c2813afc8de2e22ff9f3de0f55fbb26b3b954619
2017-10-15 08:26:22 +01:00
Equim
92c9b134c3 i18n: auto detect locale from browser (#253)
* i18n: detect locale from browser

* fix regex for locale matching

* remove debug code


Former-commit-id: 17e550af54ff213d5e2b60f83b374cd962052b5b [formerly 62fb089a7a45092b3818135dd68fac218067ef67] [formerly 733c463d2332307dafd40da5e77c6c9558239283 [formerly 4b84492a11]]
Former-commit-id: 2e117c9e060ac5cd9f80a0de2a4582eef74df6b8 [formerly 5fb6fc086bb2ebeb49a578042240a26a7879a4b8]
Former-commit-id: 7692a4fbb2889acbd7c6ee09ccd01a234998616a
2017-10-15 08:12:40 +01:00
Geno
1737702c7c typo in logging (#255)
Former-commit-id: fd0b7d6887e14b77ab97b323f4e8d1bb51ecda23 [formerly fb4fab794ca27e8f77e0dec71483cec51e4a8fce] [formerly df9da803a4301a8b1e5ed93f1f3fd121b8a3bf49 [formerly b93aed98b1]]
Former-commit-id: d17f1d4be63659781b4254b9ec36737d2f794008 [formerly 27cc8277db82e0ae123d2b1ffe95bed297c6095d]
Former-commit-id: 8137dd86f6f3caef4636416f6370bd686773df3a
2017-10-15 08:06:53 +01:00
Equim
ece52ecf7c Don't expose error (#254)
* share: remove share link when it has been moved

* http: don't expose error


Former-commit-id: c7f1d28117c770006132c75e5950d73aa9d87a12 [formerly f29a0260622644a79ff401263ba4efb143dea23a] [formerly fdd741679f09c72a121076e0a62a0d2a6eafefe4 [formerly 538b99ee77]]
Former-commit-id: 5ec9b62254b0cbc233673e7e196a5a4ece53a3f4 [formerly d26d45418267d11a1a211ba90a8b68b5b9fad714]
Former-commit-id: 57333d74cbac7088c6c527a4fe757af427dedea8
2017-10-14 10:07:01 +01:00
Henrique Dias
a7d6a72718 Build Assets
Former-commit-id: aa40a922e2dbd104bed161fd6564343c64f2fb34 [formerly 16a73ea595678b00ab9690338015746a8f5e5f02] [formerly cf4ef0a6fe12d211a1e905047a3a2cdba12e9c84 [formerly 7f7d536c5e]]
Former-commit-id: cb509a9c9961b4c8d2c6f8f67b37f30b2732eef8 [formerly f7465805945f0c08312c31bb067e3d9546d883bd]
Former-commit-id: cab59e68c27cd7c180a8964889c759efbbff7616
2017-10-09 08:28:49 +01:00
Henrique Dias
3fa9286238 ViewMode constants; default view modes
Former-commit-id: d51ad4b2671c76c3a493daf93fd4fb94a76826ed [formerly 576cfa8ebe9c66c9dbc1c8f79ab8ba7fecbe8845] [formerly 0969e87e3c070763d6e58f1d09f815b772814e4d [formerly 8633e9677b]]
Former-commit-id: 4dad4cf6bb3858122cd4d4c6a574e691788ef9ae [formerly 0b356f24a005c8886bd2165884e363f277456d8f]
Former-commit-id: cadb7589dad74e9bdb47bf3664f98ac672940fe5
2017-10-09 08:26:05 +01:00
Henrique Dias
ad5ff4cfe0 Close #248
Former-commit-id: af79c3515a4a6a6d5b72da1193b888b7cd03b286 [formerly bb0e1f8e966e238feacf3013eccf9b5e711f7061] [formerly 0f38f0df6f964f88310fde0e59f6b3a79b9c103c [formerly 90c2de44c3]]
Former-commit-id: ac03a781bce6530b0512b014c38e8d8c77d8ec01 [formerly 5469e2322432fd1991229f909e69d70966fe8f9e]
Former-commit-id: d17517418e68671237ad845df93b62840038d8d7
2017-10-09 08:14:17 +01:00
Henrique Dias
9aee1ebd2a [ci skip] auto: setting untracked version
Former-commit-id: 89e071320621710d9f24bf4dde8827f159c64d31 [formerly 6d44fe3f3e56b82762bacce64c1682262d4d1a90] [formerly 8e0f92b3205067d4644bf2febdc160d84696a37a [formerly 769bdad12a]]
Former-commit-id: 9e00c24fdca0e1ebe71aceb84624ea0b9fb85afa [formerly a04f2e37805000e19c7dcdb7daafe2396cb8269e]
Former-commit-id: 827a695fce5d7731e58052dbbf4f5bd0e3d6f5c7
2017-10-08 23:48:24 +01:00
Henrique Dias
f8ed1b41d6 Version 1.3.5
Former-commit-id: e064282645a50d6eeeb293577da260c586d4bb45 [formerly 2a9ac206ab79a8f115ed25a3afb595663f18fcb4] [formerly 2ea943c96653266a9533d9e95776955821706ea4 [formerly dd59e3c62b]]
Former-commit-id: 4ec9b9738ee48943830b9045ae40e7c305010bf3 [formerly 7748b70d5109e788ad146b466b3430a822037570]
Former-commit-id: 0981b89cb1d7bc0da43d47ff8900eb1c67301982
2017-10-08 23:47:56 +01:00
Equim
bb9b0dfd2b i18n: Fix typo (#250)
Former-commit-id: c27f105374233923e7691327f7e6f6b9a52077ce [formerly c34be33394c74229a1029edf21d9c83d5c9dd2a9] [formerly 3f5bd5a8457310d034b7fe761beb915caf35ae65 [formerly f0609757f7]]
Former-commit-id: 82d69494e5c421a245ceff0df033529ef488e419 [formerly 910438160faa1e853c91252845443514a066b3f9]
Former-commit-id: 01874c63eb13dc39471c3c2cd06a9b2e99394f46
2017-10-08 23:42:51 +01:00
Henrique Dias
cc2ce884fc [ci skip] auto: setting untracked version
Former-commit-id: d68d28c1e0000f0308b1e84a912f00b8a91f7fbf [formerly 15e0131f25aa82b8bb340e2d58d09c35a4a91754] [formerly 282b0c8d07c82de35fb87927c9da29182efaceff [formerly 9d27c38097]]
Former-commit-id: 9cc06bb0c230f2acac72fc6b4d00e4d8446a5f88 [formerly 5634a96f7b01d6f08a6a46f24dab7a3f03668fd6]
Former-commit-id: c9629add111d645e386461016771ebd1b14b98b5
2017-09-11 09:49:54 +01:00
Henrique Dias
e7e7679002 Version 1.3.4
Former-commit-id: f7727c37ed227e062a34de8b67a5bdeb95caab27 [formerly 9202314bd4a0a0288e3b95c179aaeaaf38997f7f] [formerly c57ba427c5e85f4aff34ccc5476d2c374c93a6d4 [formerly d41715c0c2]]
Former-commit-id: 7aa4c2ee81fa7dd451fef64198335f7a6ee78173 [formerly 4d1553375b1a3c86e9f8de45ef658ff8fe64aba4]
Former-commit-id: 85340361dc98b2ce775d1c41291ff3af165f99ee
2017-09-11 09:49:42 +01:00
Henrique Dias
9a829fd594 fix #241
Former-commit-id: 8578bf0b790ea4b8b5c5da4876fbccd2ead42d3c [formerly b67d35502fb0c9a3c57226b812dd2b869c5fcae1] [formerly 506eb279c974a86b232be57f87a11ec283b3b742 [formerly f658394dd7]]
Former-commit-id: e9565ce6a4ef229943f132fb6e05c5ec853447ed [formerly d0b2c24f6df2a74c403ded829cfd0746659e7d5f]
Former-commit-id: 2f4afe92081915821dd5b2fe745faf1492bdcabf
2017-09-11 09:46:17 +01:00
Henrique Dias
624d61930c Add ReCaptcha to main
Former-commit-id: 06bc7079f6d939e5531a3d9600052f979adac86d [formerly e6d8fa4418ccfa8f0163530647099432a936d4ee] [formerly dbd0cfc3770972afdf7aae8121d3af50681d55eb [formerly 879ad7b518]]
Former-commit-id: e6706456ba300c501ae66664596b5709e45d87df [formerly 98238eb61781a545ee4bac512a4f02257f4cf165]
Former-commit-id: 30bfb2b201ecbbd1ac2cf58cbadbe82daea793cb
2017-09-11 09:23:59 +01:00
Henrique Dias
ee30e7711f implement recaptcha on login
Former-commit-id: d7495b6fff4a99a8d155a3be87b15535a74a1305 [formerly 5b3a544447cca0d1cdcb6c87ca94f450a5493506] [formerly b4de1a4f5d4dd295c98366ede2b87bf2cb7918f9 [formerly 002f8066c7]]
Former-commit-id: c0e5d38111a99f8e3e71fb5db86e19b7ba44ec48 [formerly 1b5e454263ba64ced95c6d4b51f5f32e66f74758]
Former-commit-id: cfb17a53fc86d0071fba91503502444f5f10a0c7
2017-09-11 09:00:59 +01:00
Henrique Dias
6e5116aa27 [ci skip] auto: setting untracked version
Former-commit-id: 4f0cecd21f2f1fec680773c6242d6ab9687384f3 [formerly 1dec18820a98ad1ffc39eddff12167b4d1564254] [formerly ffdde8ede3a4d934d492224a046b654518cffbe9 [formerly a61329843c]]
Former-commit-id: 865c60a764807ea8d6781ac4bc95a4c8ddd9ef56 [formerly c9799373cf5e9637c78faffb94117c355a3a4f8c]
Former-commit-id: 070a498ca4c9d6da545c9b10d60e0f84e7e56889
2017-09-10 10:14:35 +01:00
Henrique Dias
34acffbb7b Version 1.3.3
Former-commit-id: d3126ad3137ddb4270199227c7ac4a0aa248b117 [formerly d8de0edac44991fb25fc3d58bc5fbf34ca51e14c] [formerly 65f8bd6a9c9a15f2182f7d9d0d0317df8a7a636c [formerly 53dedb7f5e]]
Former-commit-id: 473f94e0b181bfe06eaf73648775af5e89b03032 [formerly 9efded6125dc4ef0384f441933139847513fde51]
Former-commit-id: 8ada7c0571decba2abb0e721650401f040a6fb86
2017-09-10 10:14:22 +01:00
Henrique Dias
eb6f26c191 Add view mode on users created on previous versions
Former-commit-id: 6c3e320b2004beb8172e966e1a7f1885e33ed20b [formerly 829930444312dcfa1e144b3631c56632f6094d01] [formerly 68ae2ee9df5b3908fb30a07094f7d464818a0d52 [formerly b651d03ea9]]
Former-commit-id: 3af8ad9075641ca34504e33a2eb363eff7c6c63d [formerly e2873038181f9cc6f8d1c465aa5544cf98eb735d]
Former-commit-id: 584b5c3452545d2a6361ade02e093fe5596a52c3
2017-09-10 10:13:10 +01:00
Henrique Dias
06f3e9744a [ci skip] auto: setting untracked version
Former-commit-id: c7af338241ab6cac5e5e6f5683a8394e30a1ff6b [formerly a28f7d51e97810acd69616c22eb578f2d65e8528] [formerly 6b372592bf387d73b10e42a29ab75a5b99647d65 [formerly 750862b17d]]
Former-commit-id: a6a9cedfc2f7507080b39c665d97e8b586a746de [formerly ceeb7a7fc060245d21d169eb22b6b099fc4c384d]
Former-commit-id: 5dcf4b4c5a1caf3b482baf7f0f8aecb1b69a9263
2017-09-07 18:28:22 +01:00
Henrique Dias
cac5413424 Version 1.3.2
Former-commit-id: 90d5ad658b45bedcba302eb5ba31bd419742b6da [formerly 7fbf1df2fdf4319d6079a844d03fc68d0e6272bf] [formerly 8c9bc269c306ba49250a959200b86f1035ff5565 [formerly 6fce037280]]
Former-commit-id: 6da196d1aa360654f7fb61f02d448e6d278d52ff [formerly 272a9fd5dc6092af922aac72040fa0fbe4ecfc52]
Former-commit-id: 07fe419998104c357feb2376e61a9d3c3a65273b
2017-09-07 18:28:10 +01:00
Henrique Dias
037efc2eb8 bugfixes
Former-commit-id: 38f7bdc4de8a29be11376e2321aaedb027b84a57 [formerly 72daa72c1b48182002bf81ac374b54dad2bae465] [formerly 8498a21191a043432e0a857cbc4aae49761bfdbf [formerly 2ad805d793]]
Former-commit-id: 068d502148b08cfd8dc627739847888f018f4417 [formerly 882e3aff3891329a62374c448e839ba41edebb45]
Former-commit-id: 7b71062e2c6e86c5e1ce36daaeb265972c941ca5
2017-09-07 18:27:30 +01:00
Henrique Dias
7de22b53b8 Settings styles; close #228
Former-commit-id: b564ba4e357b2d4b18c7f9407395894eb5e18159 [formerly 84ef220a2b10b8ed501c4499ef03d99acc148546] [formerly 00056633e5c2e947201a2dd1ad3bb937821edf61 [formerly e3212cd076]]
Former-commit-id: 866b84d788a5f8a9767affdef17806dbac984db6 [formerly a22d95ba5726dd5ea757410249bef37654213a68]
Former-commit-id: 4600591829c0a41c5de9defc3e30fbac28815e25
2017-09-07 18:19:29 +01:00
Henrique Dias
ae19731015 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: cda66995085b2fe15ba1327f5f21d6cb1e7fbad9 [formerly 1e91b652c2c9ac12f7148a5fcee6826823f861ae] [formerly 6e9c1d23074a4ef51a8342014600a64421e37411 [formerly 35742fe91f]]
Former-commit-id: aaf02c9e8fae32fe0507d4375bce87a97637ee0e [formerly 4b2b1e3a10edbe38434ba663d6888cc462afe749]
Former-commit-id: 02ad43301dbc1d5dcfa4e9dd6aca07689d7df378
2017-09-07 16:39:03 +01:00
Henrique Dias
8aa0797019 build assets
Former-commit-id: 8ac0fd77c313bc4527531ba8fcfbab0723108d4e [formerly 83f9d8ce20bacf6259216c0d902957e4c2b9d798] [formerly 04e9fd8ecdf88a44f562da449ff08bb16c108f5f [formerly fcfc28d09e]]
Former-commit-id: 36785e0d78ac8f7af84f4a1071d4bf226eac2366 [formerly 874b8426ee0aa485fe29b962c7ad6e01d90758ad]
Former-commit-id: ed6bd42a0bf059265816edb0085abbc9fbfda4dc
2017-09-07 16:38:58 +01:00
Maxime Daniel
17b3a403a5 Add new triggers and improve environment variables (#238)
* Add copy/rename trigger and more environment variables

* Add after_upload and after_delete trigger

* Fix scope since merge with master

* Add new before triggers


Former-commit-id: 65f7d47840980e5e9f330ae29053b37f2b98bcdb [formerly 9ec3d017d7f072d1bce2492c3a3e9839aab679a0] [formerly 2359047e3b51594ec705f2baf30b73f3bfc4c6cd [formerly 51295f999d]]
Former-commit-id: f128ee9a69ee280f0488aeaa6c8d86337427bc80 [formerly 311dd0b0207a58427d51a07244c3b0ff310dfc6d]
Former-commit-id: 734321b412e9ec7a5d848f9579e134ebce58145f
2017-09-07 16:38:32 +01:00
Henrique Dias
819d511690 materialdesignish
Former-commit-id: 5624723eeb939734902eeaa6c2f132c4beffa911 [formerly a83456d3eda5175e2941b2deeb58b5da323ff678] [formerly a6b3ac7942dbf48d7d9b3f8db5e5041a93143f19 [formerly 4d6b54c63e]]
Former-commit-id: d03621c16b2c892701d678361b6c0a7d5dbec620 [formerly d818b6751e035f283e1c8390d7993a33a459a7dd]
Former-commit-id: 3539ee68532135467daa2cc175482769b1efb592
2017-09-07 16:37:11 +01:00
Henrique Dias
30cfd06e3d change location of database docker
Former-commit-id: f4b3c8ffe4d6772abed3064173ba0ea8efaa806a [formerly 4792bef0a22c9a132ef1a8d239e92bfcb319c38e] [formerly b0de425e325346a805635a3907f3441556ece17b [formerly 6d2fab77e8]]
Former-commit-id: be1ed94475486cf7d44b339f11b232e284efe8b9 [formerly 167c46931b5751dc7455551cadcc075f646742a5]
Former-commit-id: e8ccb7b598697255949abdcfd6ea760fd2e401f5
2017-09-07 14:19:05 +01:00
Henrique Dias
c5558e6e41 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 068b3d896b21bf786e9a2b7758eece8fa5071058 [formerly 7d80cfb73a7481dee4c95c0f3c39a95d13fd2f7c] [formerly 2b4adf5432b9fd7bb49b244955676db4ba6c5fdd [formerly e7bdba8948]]
Former-commit-id: 4f37bb0dc50a5acbf8956f0ef19d4ecebc64a534 [formerly 4eb5ba6be4bc857fe05862485d76e060bcf37fe6]
Former-commit-id: 98af155a74f74287f36fa30d29b8036c73fa1302
2017-09-07 14:18:10 +01:00
Henrique Dias
c3b3099ebb 1st phase - Global CSS
Former-commit-id: 508c4ab746f994bb3f4f5e86ff1ca5e6dc873f3a [formerly 8a29c22f817f54abefe21116e46f21b24306b6b8] [formerly e23c1a85571f61877b67656ef361f1c15acfcb3d [formerly 67fb6f8a78]]
Former-commit-id: 71e9024cf9107ef4e40f95bd388068fe052ea4f0 [formerly 7861ffe8d20f0777ae48c4f159efd7e32b2204d9]
Former-commit-id: 7db04ac5016d182f13ac56911d0a10871fb261f3
2017-09-07 14:17:56 +01:00
Ricardo Gonçalves
b1d47daa69 Docker image built from scratch (#237)
* Docker image built from scratch

* ldflags aren't need anymore


Former-commit-id: c527464833492254cdd14ced0f182fd78fbf4dda [formerly 5d99678c525e565d5b39f3bd645906b4e997752c] [formerly 80463ad3ae8e60456d5725277125c800bb2d6f50 [formerly 55789107e0]]
Former-commit-id: cea3a7799a9bd767008cb1b854314f6414892aff [formerly e682f6a01994537199f8c178de7526b961591d2b]
Former-commit-id: e76349c3eef778dc5913fb7128ff2b69907e9433
2017-09-07 14:15:25 +01:00
Henrique Dias
f51e2d5ba1 fix #234
Former-commit-id: 7e0b0a321a0bf352eb3530fc8d1250ba04499c87 [formerly a431ecc1f8f4d79b6ef98529ca38b7a00ba332d2] [formerly cc09c86c0b996c5c01bd4b7e65671e545e3c1828 [formerly dd7cd110db]]
Former-commit-id: 7bd178d355de3228aa03ec3cbb814e52588ea7df [formerly 35095c2b6728944bea8173c3ea1904c632ab30b9]
Former-commit-id: f2e8e8539a5ffce98ea186f42fd183710bc2f9a8
2017-09-07 11:42:17 +01:00
Henrique Dias
386974657e Build assets
Former-commit-id: 3ac594e7a4f3e806f56ed23790a1a195c87d941e [formerly 6e6b55d1430bddce55b701c04da53c1d746b4168] [formerly e3b0d8c63cf8bacb88c90d2f4e1c5163a448e473 [formerly 59b99ee298]]
Former-commit-id: 5d32e7939ed0941e60ec56f5a7f21fd78216706c [formerly 9c8c8a52a0633de356322fbdc0c54a9f5395daaa]
Former-commit-id: dbd51a43c6e8bc3df50ae57aa98e60bb611ab066
2017-09-07 10:43:32 +01:00
Henrique Dias
f7858cd719 Default view mode
Former-commit-id: 9212e217514046246a2bde10319763b05998b223 [formerly 8643e0ba3b4246ac0320b5d96dee4ba95e74a346] [formerly 77adb69159dce9cc89b631107340812916a2e279 [formerly 5daacf0298]]
Former-commit-id: 1a264e8a297e2a5146db53cd6cc40b8baba778de [formerly 607685afb5b6c89703c0b6d82b3dcec07eb22bef]
Former-commit-id: 1732f02d1641236402976ae182ee03f7b936d0c1
2017-09-07 10:41:01 +01:00
Henrique Dias
b355a5c058 Close #232
Former-commit-id: ec2f7562e0830ebb98bc7b4d997d74f2e6d685a6 [formerly 281a652559131d195b76feefef5cf0303d312b1e] [formerly 11a192c2d6f5d9667c55fdcf3f028391f70f6793 [formerly d3d3cb3d4f]]
Former-commit-id: a55ef038e404c2e9b97a802bfbf562fac02a98cc [formerly 033ded413b4f7bd21b7ff60feced6b590203cc16]
Former-commit-id: 6fc7fedc5fc087790102a07c3db3060e82400411
2017-09-07 10:29:19 +01:00
Henrique Dias
50758b53f4 Autoplay music and videos #233
Former-commit-id: e6305724cc9b53b764ffc199cb0b114e4fa63984 [formerly 36e7978a12f653f156c01ee11c2f91d24f7a7ca8] [formerly 9927ed89f8c30a24141865a3a7a74ffa79180853 [formerly d555cc44a0]]
Former-commit-id: 42efd044dc45d99a8fcd16c29a1b903b54f99a9b [formerly 92f6b43d0420b782defa61c34d2eb5ae9e9244bf]
Former-commit-id: 8fa9ecdbd6cc57a0513c9a72b5ab0b029839af41
2017-09-07 10:07:24 +01:00
Kyle Bai
f9902d2bdb Update zh-tw translation (#236)
Former-commit-id: e2e8b607eaacbcea36a8d3389dd4502158d2ac8a [formerly d47475c6aab73ad3c0ddb08e90664a0d3d2d9547] [formerly d347499f05216a64cd8af1075525ca6b3b7c3c41 [formerly 4d1b2cbb14]]
Former-commit-id: 229b92d7ce1143d6f9768ef8f52b18f5ead937b8 [formerly 673c0452c0d7a677a7c6fbbef941e47701bafe47]
Former-commit-id: 7e8d93b3114d545139cb2f436cf16bdd481713c7
2017-09-07 09:41:03 +01:00
Henrique Dias
b1512d2b66 remove served with from translations
Former-commit-id: 8fd3db2a2303fd77e51230e4ca9336013021d55d [formerly 3a6d99cad26160eda920508962ffa4f23fe2acd4] [formerly 969fef4feac9234ac7d165e4b8c2d572cf41e493 [formerly cbba245f84]]
Former-commit-id: 85af99ac3e6c48967e1c5d11e0fc6403492dfb0d [formerly ef7e5d4f0b655ca32783c21a6678f5e2c6ff8a1d]
Former-commit-id: 88024e6c39480df5799d892f1a7517b5345b7e22
2017-09-03 10:54:48 +01:00
Henrique Dias
1cfd31756d untracked version Sun, Sep 3, 2017 10:51:45 AM
Former-commit-id: 3f9a066d3456fd0435b5df19a6071f403761c363 [formerly 7ed9f0a686c30d97af32dfb7fa35b1f4dbbcc9ca] [formerly 8f473d98da1799211e7e7fd4e26dc9481297ea7d [formerly c60e7b9c0a]]
Former-commit-id: 732ae3300efa4444b06406743f9b8cce813e6b99 [formerly 465966ff48d99ca33496ff9dcf2d51e375985d0f]
Former-commit-id: 8c61fc2054cbecd2f0b7354884634f4b73f6e77f
2017-09-03 10:51:45 +01:00
Henrique Dias
9152c77543 Version 1.3.1
Former-commit-id: f16b67a8bc80baf5ed8e6e3e4764c0219429c681 [formerly 1d264f27c55881673c8cf7597c49859975e05312] [formerly 4246582d7a7292c037733c0c0f946312241e4bfb [formerly 17cb3dc1f1]]
Former-commit-id: 61269ea3228995bcd72b721529b25ad0cc94bd36 [formerly 3cce1019691228714433106d6e6c83490f36217e]
Former-commit-id: 6d51d1908e6db75ed0e614e273ac108aa9774d79
2017-09-03 10:51:33 +01:00
Henrique Dias
c236db329f Add FM version to interface; close #221
Former-commit-id: 0d8cdc850ea970ecb73bb6ce5af85ca517dabba2 [formerly 9adaffca71250d6e350914e90965ea07198be2f0] [formerly 4c982e090fe2809829c35d574b75023ec384ba26 [formerly 59bd6e97bf]]
Former-commit-id: 959919b2c265757a9dde9f34ae06373468ebae0a [formerly 120c5d413404a70885ea597d93469cc3b93bf26a]
Former-commit-id: ebf8abfcf0003babcad11185da96dede783d25a9
2017-09-03 10:50:49 +01:00
Henrique Dias
bf6e0abd96 untracked version Sun, Sep 3, 2017 10:45:00 AM
Former-commit-id: b27d1ac3dd03bc3c6e4bafc9947d57ec8ff447b8 [formerly cc8278a69ea0ff56f370fda951fbb504b13d9938] [formerly 2553bd5a8c7f8c621080d7d50a89befb1aeff4b3 [formerly 011d5aee46]]
Former-commit-id: 5e67474dbcc85cf6fccefbbc8d566a3aaa4477b9 [formerly 63293531e894499121f1a845311877f62c5addfd]
Former-commit-id: c315ae84528e06a285e01d0cca740cec071d4a87
2017-09-03 10:45:00 +01:00
Henrique Dias
8d8c756233 Version 1.3.0
Former-commit-id: 77f5b5b6d73cf979c074d4390a55b14dd9fb8c39 [formerly 4e77a8d13ec4233f1b38ec9b26e7a42bc6f4a9c5] [formerly 5d9600a5fd5b8044ea0a624570be5881740aa3f0 [formerly a4584e7a55]]
Former-commit-id: b8832f925ca7d7daa162a46b87c03b2fe32f67cf [formerly e109aaf234b60cb046c9c6dea2d96089581066a0]
Former-commit-id: 8916c596e729dc1d887da2135fe7cb7e997b03a5
2017-09-03 10:44:49 +01:00
Henrique Dias
d5f0471ab7 untracked version Sun, Sep 3, 2017 10:42:09 AM
Former-commit-id: 682aeae0dcaeac554de9fe24e50a42e53011cd76 [formerly f6147413fe2dc33d356fedd50644d2471e139871] [formerly 1d90a78470bca90eba6c0221fb06af2a10b7cfad [formerly a3f06b526a]]
Former-commit-id: ba046d6e696866a4c32fb33fa4de545aae29d91e [formerly b2e130a7078e4ac95dbdb37911ab6514f36e565c]
Former-commit-id: e13622c99901155e1d2c4fc67b75b320bd3ab1db
2017-09-03 10:42:09 +01:00
Henrique Dias
f7aaae3f63 Version 1.3.0
Former-commit-id: 474be62198116810d28ffda109669b686b66d2c3 [formerly a2c2f34340b407679035f03a41cf75f6b75f80ec] [formerly bd04b4c765993602859a3eb5e72f9761ec16901f [formerly 6b14dc336e]]
Former-commit-id: efcb629aeecb7d61f4db93be32ae9b574d9385bf [formerly 951ea5271bfad7526aec3da1ef4afdaa7004fe58]
Former-commit-id: 1e0c3c13dc7ea0247c0a2909b79f9417e1cbe59a
2017-09-03 10:41:56 +01:00
Henrique Dias
2515819026 Add version stuff on root
Former-commit-id: 4dcf3117e274fe2b656af6fd2ee98db386143620 [formerly df3bd216ef1020e9ca6a0ba57ef1c4bad5a6e04d] [formerly e5eded7248098655cd60112a6ec5fec06fd99ff2 [formerly 044575d2af]]
Former-commit-id: 8fe1a523af24c490eef9d7cd2b1bc3de656002de [formerly 73aea3062fa487a5c6c89f8f04bd86c177c783f3]
Former-commit-id: 6fd9e5260feacfe6b9d96515a7c3ab28c31f17db
2017-09-03 10:41:14 +01:00
Henrique Dias
7ad727d27d Fix #227
Former-commit-id: f1bc07c44f53df9cd1d67b870c5bb477e341abbb [formerly 4d7dba923461b33e4fab40c912458901d2295f08] [formerly 574b5b5b364719ea1939ee232e598f12697fea2f [formerly 96a5226076]]
Former-commit-id: a2262d2112a5d1ccd094d9077233d043660a9100 [formerly 73d2cf6e74cc635bf40bc4b89204c754d8c21da2]
Former-commit-id: 219e96d7c4b1da662643e48dbfa6e97468d9c0a9
2017-09-03 10:06:25 +01:00
Henrique Dias
adbff03274 Add tabs on settings. Close #220
Former-commit-id: ceb274e0e526378d5ab449470c122f11789200dd [formerly 63e043d75c173243116fb35ba389141e323e7606] [formerly da9fc0747c65a3fbdeb032a5e7e3b9e146f409e4 [formerly 9a9b41dca8]]
Former-commit-id: 2a287ab5251869f0e2e2fb836268fb2b1682b81d [formerly d7201a4b07230d32422940250bf0e01b430dde9c]
Former-commit-id: f8f4013fe9eb03ce233738e51ec2c967753b29d7
2017-09-03 09:54:51 +01:00
Henrique Dias
6b8a65382f fix bug
Former-commit-id: acb5afb206fb81d285be03b7fa93dc4e8b32fafb [formerly ff9425ef412822c13f78ddb04bae862ef0f4254d] [formerly 78a8707fa8eef88ddfca44ea61d1409326dc2183 [formerly 359e311da1]]
Former-commit-id: fa4a813c9bddc8b4ef88c3cd24b9f3fa84cb5f8f [formerly d18bd2c49ad25547fb2bb66519221b6d7fc72849]
Former-commit-id: 0a6e241fd1ef05bdbed545aeef76b442468710a7
2017-09-01 18:18:00 +01:00
Henrique Dias
4f7e6cbb52 prefix url as a flag
Former-commit-id: 837bddec0e006860889cebfa74e476aab7f1b04f [formerly 681933b995c0d65da501270f677c6dec358da62d] [formerly 7db51254915e0eeaf7e7098fb5ee79425aae9514 [formerly 3fde3c716b]]
Former-commit-id: 45535dfac3fbb2cf605d107bcb1887bfc3809cf2 [formerly ee5d2e0d7df8606566988854b0de22303a32748d]
Former-commit-id: 51378e5b5a6325938d00416c200053f772ca0b61
2017-09-01 17:48:15 +01:00
Henrique Dias
40fe081962 fix error
Former-commit-id: 6f9d7c0b4ee819f737395ec1b721a88be1e6c5e6 [formerly 0489f9f724d1165bf63c45df84398f9631ed9833] [formerly 5a3ffb16e3a801dd811b3d05ce853413d1da2ae6 [formerly 5c2166bc15]]
Former-commit-id: 4672f5e091c847482af0ae4741bcb809134a1559 [formerly 96ecde75a3d61b9cc1e77eae6254166ec53b902c]
Former-commit-id: 683218b398c699cd0f6ce15bebb6385d20eee390
2017-09-01 13:26:04 +01:00
Henrique Dias
ebf1325126 build assets
Former-commit-id: 5fd962f1a14d288995f97fdfd22480c0af54cf80 [formerly 1642bbafb9bdb91a58a5fb1532b1168894b5574f] [formerly ad423b6250b27f4eed0d9cbdbe3f09155e343ef0 [formerly 0ffb1fbe39]]
Former-commit-id: c3c687df2340c41f44356cd844a0b5ca0d779f71 [formerly 7238fdf7c28d58a734d4d6d8d71bd47456064dbc]
Former-commit-id: 5be42b6cb65f638508133c56fc6775d71e1dacca
2017-09-01 13:22:35 +01:00
Henrique Dias
ea1b9febb7 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 79aec32eb9ac9b7e75ad00689990a865d5fed5c7 [formerly 96b947bf637adadc04409075b715ee85e4c7376b] [formerly 384abd27c0d7d22e67fd70b57c764b2dcbe69611 [formerly 349b3d02e6]]
Former-commit-id: b58b2e6bc0a7e90a3a77054c686c98f2cfb96978 [formerly 8a6ddadf8af01a29380dad9fa6ea4ca035aa8043]
Former-commit-id: 31856ee921c1fb7d77ba7704ea0af5e2d58ff952
2017-09-01 13:20:11 +01:00
Henrique Dias
f3edf63fb2 fix #226
Former-commit-id: 7a2d0e67b2320af58b04da5e5834c0ed6a0ec137 [formerly 468e34a7bbadffdfc268c072b4460c99beb066d9] [formerly 7a962e989a213218868b02cfdd7c289ca0b6831e [formerly 2a5aa21b8d]]
Former-commit-id: 2f443bbdf9518e66136ab97cf8c2f7e434a65f7b [formerly abcf5d2dc1aaa8b034bbe7b958cdec14feaef1b4]
Former-commit-id: 22cf31bc4973b51fbf9849f537e0191c65bf327a
2017-09-01 13:17:10 +01:00
Equim
f13e7ba940 Update locale (#219)
* i18n: update locale

* i18n: update locale


Former-commit-id: f2e21d0882f071f86bc3c225067313e54bcab1f4 [formerly 8cf56e48e3c40cb7bc1f9df58daa14ebc1d4d459] [formerly 48628588159737a35937da4511c1680600c97f51 [formerly e910f8580a]]
Former-commit-id: 2b0669041ae6f9c48d8163f3e20f69636a9ff291 [formerly 0a23672f8298176a8a03564a20d978225ef1bb6b]
Former-commit-id: 6d9a6057a6c67d645edf3fd617663afa1f42f977
2017-08-28 19:12:35 +01:00
Henrique Dias
3ebe219e96 Option to lock user's password #215
Former-commit-id: e4f0afef51c437bd55c58f12f2f45ce8e8c84bb0 [formerly 549235edf9d6c43d1454a8a00d7b6f832bb8a3ca] [formerly 12d099aa44bff7d995b05680e405d8040f1e1850 [formerly fc9ca4f6a4]]
Former-commit-id: e9666db20e2b473095f21c03d59f2a8fbf07929e [formerly 87ee9eb83daed5180c6a3714c0ddc861668d747b]
Former-commit-id: 5a15b05320c1eb28324e50cad7ca980d3eebcb02
2017-08-24 14:44:53 +01:00
Henrique Dias
d838856711 Popup on copy link.
Former-commit-id: e483df4402733b102d11b10436ff74aad11dfa7c [formerly 6d761c2ee838a9766f755b6c54cdc2ca388b5934] [formerly 1365e9e067af021ad0c680bae3af963dc4a90b28 [formerly 889871ec0a]]
Former-commit-id: ba443a90fded4501c0a6872eb293c14b2923c627 [formerly d21c6b9ab41869d2b10aa99853bc5b6931b63d96]
Former-commit-id: 7c19b231861797c62dc35c1e8a28f4ceeb8761c7
2017-08-24 14:13:29 +01:00
Henrique Dias
610d55c26f Fix override.
Former-commit-id: 515874171ed00bf70db40b2fc302d479d18fe93a [formerly ea9e47252daae3fb211150e2c3ab67ac97067a32] [formerly 2b0378600c957da5e2610f3cf8e82dd079d11f65 [formerly e285195fe1]]
Former-commit-id: 05d03bb250d4bc6bc4b3dbaf388f79a682f09841 [formerly 4ef3ceb2dd3862011a09ae39fbf4a78b889100e4]
Former-commit-id: 448224f282d6b14678f8d5d6629cbbe44ed19644
2017-08-24 12:33:54 +01:00
Henrique Dias
ac044016ee Implement SHIFT+Click to select range of files. #206
Former-commit-id: 15da15e0588d5977b42f71b8c96980248db73889 [formerly 95e464410bb619f4ff70b21d846e937bf9355e8a] [formerly cae63a6f6de24229ff04392fe16260c0d9045e8c [formerly 297a52e606]]
Former-commit-id: de9d291dba6c2e2d750c519f1fcbd59c34538ab1 [formerly 4fb3fcf8ba5726f6dde56fcbd8c1457799539a8e]
Former-commit-id: ad1568f33fa927dc750515856950b95b588d9f68
2017-08-24 09:14:26 +01:00
Henrique Dias
58af3461a8 Fix #216
Former-commit-id: 6d4b0674327746ed46fa4397f2d3ab01b8579bc6 [formerly eafa0d3effab4921e1a57de23f5a189bb75dced9] [formerly db40d686eabe4b78d9a5e8a6b1c20b623880d986 [formerly 9301966eb7]]
Former-commit-id: a148dc60ea64d054567f6d9b5d0c383893851da0 [formerly 02af0aeb73dd88cc157e5e21a9e9dd3658316d2a]
Former-commit-id: 1d4e9c50e20d3689f39224308bfadbe6097f5664
2017-08-23 11:05:54 +01:00
Henrique Dias
4191a6f9e0 Fix login issues regarding basicauth+noauth #214 #204
Former-commit-id: 5c150c622724e7d2c9a9630f60325a5a246b7526 [formerly 6ff5856dc0ab40dbb9a9759c0c23de74a23d2531] [formerly b41c082f0631a33648bc50e81f38d7dbba612d8c [formerly c95b1aaace]]
Former-commit-id: ead207a0e5405ccc0641b3705875b8846846060d [formerly 7c1221485698d524ef679d09166cff7549f7b00a]
Former-commit-id: b01bdc787983c2c48394ef9e3eabed9137c1f31c
2017-08-22 15:25:54 +01:00
Henrique Dias
c18ca4702d Fix #213
Former-commit-id: 57732db98608e0b8fbe0e18f6d77d69b7cdc8807 [formerly 647558cec7e30388a3dc80c002bf2787f27edbf7] [formerly 03295cda86a4ce43a734c4617d7d4429ffeb7570 [formerly 1aa68b0dd0]]
Former-commit-id: 52bc1fdecc89456f7dc32f5ce64a40727070d06c [formerly 8a1a04c67bba6988b448f9a4c612ba62aeaf7e62]
Former-commit-id: 9b4565efca09847c31f6f856eae1aa488461046d
2017-08-21 07:33:16 +01:00
Henrique Dias
73b1094602 build assets with french transl
Former-commit-id: c3153457b2d0b1011820bf8903bf70026625ac1b [formerly 99d342f928dd31b770a37de1e9f3b743b972a13b] [formerly 2a8ddc01230f98a71b1a929856b2967472e11188 [formerly d53b5ef445]]
Former-commit-id: 9e7799705ad69806841b7366ccfb4ae7433c248b [formerly 4eb717f55bf191a36e35af721f11e2ed242668fa]
Former-commit-id: 7134a11b00ca44865a735f2e8bdb1563318f391c
2017-08-20 16:52:26 +01:00
Corentin Potron
5d026ac15d Add french language (#212)
* add french language

* add french language file fr.yaml


Former-commit-id: 294d7ec69dfe6c81895256b503a44f14bd4ba879 [formerly 0680c3461994cd62a6e1ba5886668490a37165dd] [formerly ca03271d879cc8e9520d8cf5954539e6470bf283 [formerly 791607932f]]
Former-commit-id: 8f12483066aa0519054195b69a30a91659f6a8d2 [formerly d9f47b9e50c125e5c81f4932c28570611a879162]
Former-commit-id: c8f593c505937d017eb2e0ecf3af3a958d6de5f9
2017-08-20 16:49:19 +01:00
Henrique Dias
21156bf24f Merge branch 'spl'
Former-commit-id: 22d8f06e8a8f25af502d28a913fe83fa8aaa3970 [formerly f93cb05d08b4ca935aad07262f3898bf2fce87ef] [formerly 3562ac890aa6a5ab7f5514b5f9c5617bcf16c844 [formerly bbffd10e0b]]
Former-commit-id: 301bbb736a3003ef4b9ac0398e6c3b311a9281bc [formerly 9ed7c705279356410e9861c2100b0d0911acc54f]
Former-commit-id: df715dab654dc2116dfba8bbea3365d940ceecb8
2017-08-20 10:27:09 +01:00
Henrique Dias
edaf6d27aa solve conflict
Former-commit-id: dff18181a89dac3406aa57f5b599b3cc8d25dcb8 [formerly 3aaa6e4e972c5615fa802475379ad5f9d052cc0a] [formerly c0fe3b069670959a1dcca83ce22f4b1656ff3352 [formerly 76bb197dac]]
Former-commit-id: 573869a30349cb42e65dff6945ae08cde2715b36 [formerly f1d63a9fcd70df30cecfd1ffb85f80a06508675f]
Former-commit-id: 21b95aba2cadceee0a75db85671706429adee228
2017-08-20 10:26:49 +01:00
Henrique Dias
816cfb2a6e Fix one bug
Former-commit-id: 3886c36ab7a1a078f0f740caa7ca986490f50f8a [formerly f71de42494da68fac79b6618e03aa44ea8cf7d5b] [formerly af6208163a5bf54fa06171990cc8adb8f90f4834 [formerly 70751c48d2]]
Former-commit-id: 57015ba46af265788d5e1189e7f981aa3e9c2c2d [formerly b0a2660aaed889c9f36e3ba4e10f4a703e8763a1]
Former-commit-id: 33279da11c9ce00ebb77feef419d6f6d82d9572a
2017-08-20 10:23:04 +01:00
Henrique Dias
269ec9ea4b Update docs
Former-commit-id: 70685b598a006623f25a6774fe6084f57cfbe1bf [formerly ccbb5f53ed5d72472604c68e47aa07761c0206f7] [formerly 0b34f9e46762e5b6d940f66f6f1d3a1855d56915 [formerly 154bfb19d5]]
Former-commit-id: 9b20eb1064d9ca96d5100c3ea77b52e8675a393d [formerly 3ae727e5bc065815b79a9df4ccbe4b39b0a6a4e4]
Former-commit-id: 7e5bd07d8310d5df84a0490c7cb75c94e5fa3026
2017-08-20 10:19:04 +01:00
Henrique Dias
9186c1f36c remove old test files
Former-commit-id: f84bd9945948105adbad9e1adec4cc072fc3a083 [formerly f6f884ae5c22aa3f51ffffe7828a892a09b8c285] [formerly edac9defd87c75efb7fbb86a5fd6aaa2084bbd10 [formerly 0117cff0e6]]
Former-commit-id: e5093a9010603aff9432dcc332e7de3507b8ec38 [formerly 95474b25e1c766a94c32ec69136f261bd0d908b1]
Former-commit-id: 0029467a0059d12c2dbf19ac0339a4bc6b1185b6
2017-08-20 10:11:31 +01:00
Henrique Dias
d5cefa20b3 build assets
Former-commit-id: 695d0f7584c1ee104eee182bd9fc909128d76d64 [formerly 97fcfda710cb87cf9c30e3c7bdda343b4676bc60] [formerly 732c84c6355d9585957fe54b11b882bd42168b12 [formerly 0ec21987ed]]
Former-commit-id: b0741bfb46793a3907b260c55ce524ca68389ec3 [formerly b80ea86d170af278d273a7e0225e4b43cd7d874a]
Former-commit-id: ab876ba5754fda34ba3933d2b8659bb75cf08324
2017-08-20 10:11:12 +01:00
Henrique Dias
1be7d7d256 Close #207
Former-commit-id: e7a0fe724406f2284d872e1b49451e8c85924bf4 [formerly 3e0f7f027d117dcb825b326e880ffa05cd5a7ce6] [formerly 70ddacbae6288e5aac8cc9d5cbb250814fe07f7c [formerly 4bf34124a3]]
Former-commit-id: d9118bd6c1689df00c53219d5910892ec68892de [formerly 1e191afcd6f7d73069b6fc79e9b7a5cdd14d2ecb]
Former-commit-id: d908f459e005c204ad8eb0ef58f2e782f9b4c942
2017-08-20 10:09:10 +01:00
Henrique Dias
1b0f67c0f6 Some bug fixes
Former-commit-id: 54fc2a2869dd625e55881818e0022f3c4ae45bd1 [formerly ddba8f0dc58999a6f483fe61fda9391da251d49b] [formerly f76423c629f671538e6c008365c8d6dc1a5460d7 [formerly 82b161cfb2]]
Former-commit-id: 34601615e2beb773bf266cdb503e3c9fd8ead09b [formerly 0f39bbd2d66c789219785b4a726297a7c00a7f1e]
Former-commit-id: 1c2e33c56af3f57f8e8751b4c43b05967f87c587
2017-08-20 09:55:45 +01:00
Henrique Dias
67dbf88eb6 Working Caddy
Former-commit-id: c463c6e5708b2cd10e7de37285cddf0c4898b59b [formerly 615fbb71576801762e831e00489c30bff189c7d2] [formerly cdd9f708fac1163bb79e619368ddd05e4b581be3 [formerly e4d345b7e5]]
Former-commit-id: cfb19f435c5d08cbb38e50ba970fc2d9474ffb0c [formerly 6e1aac15e1da1c06e41d87dafa262332b018d701]
Former-commit-id: 78cebd321e5a840388e6d4eca09e2357469ec546
2017-08-20 09:31:24 +01:00
Henrique Dias
98bea91edf Add Comments
Former-commit-id: ea1761b1e1bd9ff6eb80e06ff378d8263de86064 [formerly a4cefe6cf2f1da416ad34175bcea96ac5262d766] [formerly cc4c6afca638a66e223e64a6bffde563a48b1990 [formerly c6e6b08305]]
Former-commit-id: 56c1574b23fefd33b41d848f47201a24e75d9e6b [formerly b326f699ef7dde08a4e81b4a3a7db22902270634]
Former-commit-id: 078a180adea8d3f3f02177caf78cbeea22145d4c
2017-08-20 09:27:03 +01:00
Henrique Dias
7747fa8ec3 Fix some stuff
Former-commit-id: a820779e24a9e41ae3ef07ad6c49b3b441192b82 [formerly 04d8b699793d2f662cfd7ebbbf1118239deb0ec7] [formerly 94e538cb9690c186fc113e27c008f894d719d453 [formerly 921cc2f930]]
Former-commit-id: 9ddc40cc471fb3ea3fd0fcf811907f102fa13bc2 [formerly a5ea3e36f6b362f8910874d5d1ed087187e05dcc]
Former-commit-id: c4c4cc4c1505145d9b1c1058d94e6dc80a3cd5de
2017-08-20 09:23:02 +01:00
Henrique Dias
d0cf6c08e8 FS as an interface, close #205
Former-commit-id: 9bfcbeaf9c407044fb8eb3142f2eca65f42623d1 [formerly 2fb1a0292e825b5b86e506c70b548bc823050f7d] [formerly 8f70cdc0db6d4328d61769685bb39806999f475b [formerly 20818dca93]]
Former-commit-id: ec8e8d96356d56863d8a330451dc78dbf838a7ef [formerly 216fffad3e0d2cdc4632e8de9299666a74e44375]
Former-commit-id: 230c7bc974a86225064211178250ec072c0a525a
2017-08-20 09:21:36 +01:00
Henrique Dias
600723c224 More updates :)
Former-commit-id: cc9953eaa75e9c68abc19b40cf1c4391f1c5fe24 [formerly b553106eb6a4f00474c7c79f74c02ae475e9601c] [formerly 39ca855c5e0788008f5c671164fb6e404a31aaa0 [formerly 76de8e5940]]
Former-commit-id: 0c1a5f9cc633e40506f54d9da1420ae7c183bc88 [formerly 061569610df0c6e41830bf27516ef567c3a83c55]
Former-commit-id: b400af585254eba659078ea1f5f48609930c4ea6
2017-08-20 08:49:09 +01:00
Henrique Dias
44ab20964c Almost working!
Former-commit-id: b996f4f14f3ffd92fae77d86e92d077b35ea080c [formerly e4b74308ab158ad24bd6b3dc1ce615265f972e6c] [formerly 1ea38eac2569ba58e864f1edceb56daabff5e53d [formerly 5b619337df]]
Former-commit-id: 9117f9eeff1bbc259164b20f0561790b3c393319 [formerly c3c7b1c100c54a5ec0af528806e28b31c67da0ca]
Former-commit-id: 0d95a7f55f6f3ab9f89e1c5b34db927e5763c98d
2017-08-20 08:42:38 +01:00
Henrique Dias
764289e52f DB Updates :)
Former-commit-id: e9795cfca39aab57f4a4b604c65633958ff22e46 [formerly 7dd93e46eb6915a387dc64500a3fe7f6f955643b] [formerly ffa277f1605e46bb8c914464b1223fe029d579d8 [formerly a04ff87bf9]]
Former-commit-id: ffa472fd3b1534f64a1c343864564bbc0290714b [formerly 7074f824d7d7dd3cd74b884c9f3e96834f662394]
Former-commit-id: 4b1e0324de6065fd4daec25d463d3756588b92d8
2017-08-19 12:35:44 +01:00
Henrique Dias
741e5c84ea Fix #209
Former-commit-id: e11caa24e68b811c028762c3c89a1b55b241822d [formerly 2f77506d8eeff4578c328092eb17c98e02a12c09] [formerly 2bf12e36c1d29466a9ba97fc5820691f588f7e7f [formerly e7b8da0bd6]]
Former-commit-id: 22ba33d51961658a991a3fe5098c2923b4ca7633 [formerly 383193414db6aace9d46245b95630872f890bd7f]
Former-commit-id: 13ea3c87d98c9f00654251d5d954added2dbc0ea
2017-08-18 09:03:52 +01:00
Henrique Dias
4b602be5e3 updates
Former-commit-id: 54b88552d11f2151a165dba9debb4657dfa56cf8 [formerly 0ce53651a8e9660f9d5f977295f553b5b1d1e93a] [formerly 7ebca3a8896222091c95af86a9cf1d12550b8b76 [formerly 174330929a]]
Former-commit-id: 993d0cdb239f9969587d13a11ee8469fa8b91287 [formerly c22c911f944dd8d6597ab95589842d3c68d34869]
Former-commit-id: 44ed259fe50a085e8bcace3f1f14caafec97ce66
2017-08-18 09:00:32 +01:00
Henrique Dias
e4144ad2b2 Progress bar. Close #199
Former-commit-id: cf8ec044a2531b295b89da915cee439eba6ccd0b [formerly 6d77a48968796dc6db51602486c07d6f2ecc00e6] [formerly 4aa44181ea637eeaba8e7756a76bbf5cd47b6928 [formerly ec190d28a8]]
Former-commit-id: 5e8e25fee1ae1119d5aaf5bf25e83f258ad13d2a [formerly 574db23a689aaf5bbe533d2e322ae98fc0b6d1de]
Former-commit-id: 5b5dfebfb692b62188c216f0cb22f8cdefdd82ab
2017-08-15 11:08:58 +01:00
Henrique Dias
4071a58107 close #203
Former-commit-id: e2d0b723963d3a82ac0f9042280885e800b1132a [formerly e1ae3f4da43f0481a57c39f11735c36b33fda857] [formerly 02ab5f5fb5f4b812cf413ebc923b853d4f0b4afb [formerly 83bc555094]]
Former-commit-id: bee2ec30c9aa9619a69eaa6320822f8525a41535 [formerly 1af59077494b5e8674af547d0361f50a2ecf8f26]
Former-commit-id: 573870f4a6bffcee3e48fbc0b8349402eaeba407
2017-08-14 18:35:25 +01:00
Henrique Dias
04b6ca6015 Fix #202
Former-commit-id: 4bfdc41566b255452ba1179f98523f51200cafd2 [formerly 194853322977723818d2fcfe860d0fa54117f9dc] [formerly 512370c90e08f58f729ab1a9d03e00ab72b1da69 [formerly 0a11c52a89]]
Former-commit-id: 518476f3f715724ca9b4cfd65aa234006d3a3677 [formerly 7717d5bff8169c8ae5e31d3a70de35ffcd59cd72]
Former-commit-id: 0414a3e51602ea9d51257036c620cfe6853103b3
2017-08-13 10:12:13 +01:00
135 changed files with 6574 additions and 5403 deletions

4
.gitignore vendored
View File

@@ -6,3 +6,7 @@ node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea
.vscode
package-lock.json
yarn.lock

View File

@@ -8,11 +8,17 @@ build:
- freebsd
- netbsd
- openbsd
- dragonfly
- solaris
goarch:
- amd64
- 386
- arm
- arm64
- mips
- mips64
- mipsle
- mips64le
ignore:
- goos: openbsd
goarch: arm
@@ -20,6 +26,8 @@ build:
- goos: freebsd
goarch: arm
goarm: 6
- goos: linux
goarch: arm64
archive:
name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}"

View File

@@ -1,6 +1,6 @@
language: go
go: 1.8.3
go: 1.x
env:
- "PATH=/home/travis/gopath/bin:$PATH"
@@ -15,7 +15,7 @@ install:
- go get github.com/tsenart/deadcode
script:
- gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --exclude="rice-box.go" --tests ./...
- gometalinter --disable-all -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --exclude="rice-box.go" --tests ./...
- go test ./... -timeout 30s
after_success:

View File

@@ -2,7 +2,7 @@
If you want to contribute or want to build the code from source, you will need to have the most recent version of Go and, if you want to change the static assets (JS, CSS, ...), Node.js installed on your computer. To start developing, you just need to do the following:
1. `go get github.com/hacdias/filemanager`
1. `go get github.com/hacdias/filemanager/cmd/filemanager`
2. `cd $GOPATH/src/github.com/hacdias/filemanager`
3. `npm install`
4. `npm run dev` - regenerates the static assets automatically

View File

@@ -1,7 +1,7 @@
{
"port": 80,
"address": "",
"database": "/etc/database.db",
"database": "/database.db",
"scope": "/srv",
"allowCommands": true,
"allowEdit": true,

View File

@@ -7,16 +7,17 @@ RUN apk add --no-cache git
RUN go get ./...
WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager
RUN go build -ldflags "-X main.version=$(git tag -l --points-at HEAD)"
RUN CGO_ENABLED=0 go build -a
RUN mv filemanager /go/bin/filemanager
FROM alpine:latest
COPY --from=0 /go/bin/filemanager /usr/local/bin/filemanager
FROM scratch
COPY --from=0 /go/bin/filemanager /filemanager
VOLUME /tmp
VOLUME /srv
EXPOSE 80
COPY Docker.json /etc/config.json
COPY Docker.json /config.json
ENTRYPOINT ["/usr/local/bin/filemanager"]
CMD ["--config", "/etc/config.json"]
ENTRYPOINT ["/filemanager"]
CMD ["--config", "/config.json"]

View File

@@ -5,6 +5,7 @@
[![Build](https://img.shields.io/travis/hacdias/filemanager.svg?style=flat-square)](https://travis-ci.org/hacdias/filemanager)
[![Go Report Card](https://goreportcard.com/badge/github.com/hacdias/filemanager?style=flat-square)](https://goreportcard.com/report/hacdias/filemanager)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/hacdias/filemanager)
[![Version](https://img.shields.io/github/release/hacdias/filemanager.svg?style=flat-square)](https://github.com/hacdias/filemanager/releases/latest)
filemanager 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.

View File

@@ -6,6 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="base" content="{{ .BaseURL }}">
<meta name="staticgen" content="{{ .StaticGen }}">
<meta name="noauth" content="{{ .NoAuth }}">
<meta name="version" content="{{ .Version }}">
<meta name="recaptcha" content="{{ .ReCaptchaKey }}">
<title>File Manager</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
@@ -22,7 +25,14 @@
<!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<% for (var chunk of webpack.compilation.chunks) {
<script>CSS = "{{ .CSS }}"</script>
{{ if .ReCaptcha -}}
<script src='https://www.google.com/recaptcha/api.js?render=explicit'></script>
{{ end }}
<% for (var chunk of webpack.chunks) {
for (var file of chunk.files) {
if (file.match(/\.(js|css)$/)) { %>
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>

View File

@@ -1,11 +1,38 @@
<template>
<router-view></router-view>
<router-view :dependencies="loaded" @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'app',
mounted: function () {
computed: mapState(['recaptcha']),
data () {
return {
loaded: false
}
},
mounted () {
if (this.recaptcha.length === 0) {
this.unload()
return
}
let check = () => {
if (typeof window.grecaptcha === 'undefined') {
setTimeout(check, 100)
return
}
this.unload()
}
check()
},
methods: {
unload () {
this.loaded = true
// Remove loading animation.
let loading = document.getElementById('loading')
loading.classList.add('done')
@@ -13,6 +40,35 @@ export default {
setTimeout(function () {
loading.parentNode.removeChild(loading)
}, 200)
this.updateCSS()
},
updateCSS (global = false) {
let css = this.$store.state.css
if (typeof this.$store.state.user.css === 'string' && !global) {
css += '\n' + this.$store.state.user.css
}
this.removeCSS()
let style = document.createElement('style')
style.title = 'custom-css'
style.type = 'text/css'
style.appendChild(document.createTextNode(css))
document.head.appendChild(style)
},
removeCSS () {
let style = document.querySelector('style[title="custom-css"]')
if (style === undefined || style === null) {
return
}
style.parentElement.removeChild(style)
},
cleanCSS () {
this.updateCSS(true)
}
}
}
</script>

View File

@@ -1,10 +1,12 @@
<template>
<select v-on:change="change" :value="selected">
<option value="en">{{ $t('languages.en') }}</option>
<option value="fr">{{ $t('languages.fr') }}</option>
<option value="pt">{{ $t('languages.pt') }}</option>
<option value="ja">{{ $t('languages.ja') }}</option>
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
<option value="zh-tw">{{ $t('languages.zhTW') }}</option>
<option value="es">{{ $t('languages.es') }}</option>
</select>
</template>

View File

@@ -46,7 +46,7 @@
</button>
</div>
<div v-if="!$store.state.user.noAuth">
<div v-if="!$store.state.noAuth">
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
<i class="material-icons">settings_applications</i>
<span>{{ $t('sidebar.settings') }}</span>
@@ -59,7 +59,7 @@
</div>
<p class="credits">
<span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
<span><a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a> v{{ version }}</span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
</p>
</nav>
@@ -72,7 +72,7 @@ import auth from '@/utils/auth'
export default {
name: 'sidebar',
computed: {
...mapState(['user', 'staticGen']),
...mapState(['user', 'staticGen', 'version']),
active () {
return this.$store.state.show === 'sidebar'
}

View File

@@ -1,32 +1,35 @@
<template>
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
<i class="material-icons">{{ icon() }}</i>
<i class="material-icons">{{ icon }}</i>
<span>{{ $t('buttons.switchView') }}</span>
</button>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { updateUser } from '@/utils/api'
export default {
name: 'switch-button',
computed: {
...mapState(['user']),
icon: function () {
if (this.user.viewMode === 'mosaic') return 'view_list'
return 'view_module'
}
},
methods: {
...mapMutations(['updateUser']),
change: function (event) {
// If we are on mobile we should close the dropdown.
this.$store.commit('closeHovers')
let display = 'mosaic'
let user = {...this.user}
user.viewMode = (this.icon === 'view_list') ? 'list' : 'mosaic'
if (this.$store.state.req.display === 'mosaic') {
display = 'list'
}
this.$store.commit('listingDisplay', display)
let path = this.$store.state.baseURL
if (path === '') path = '/'
document.cookie = `display=${display}; max-age=31536000; path=${path}`
},
icon: function () {
if (this.$store.state.req.display === 'mosaic') return 'view_list'
return 'view_module'
updateUser(user, 'partial').then(() => {
this.updateUser({ viewMode: user.viewMode })
}).catch(this.$showError)
}
}
}

View File

@@ -134,7 +134,7 @@ export default {
})
.catch(error => {
buttons.done(button)
this.$store.commit('showError', error)
this.$showError(error)
this.$store.commit('setSchedule', '')
})
}

View File

@@ -7,7 +7,7 @@
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
</div>
<div v-else id="listing"
:class="req.display"
:class="user.viewMode"
@dragenter="dragEnter"
@dragend="dragEnd">
<div>
@@ -98,7 +98,7 @@ export default {
name: 'listing',
components: { Item },
computed: {
...mapState(['req', 'selected']),
...mapState(['req', 'selected', 'user']),
nameSorted () {
return (this.req.sort === 'name')
},
@@ -196,6 +196,10 @@ export default {
})
},
paste (event) {
if (event.target.tagName.toLowerCase() === 'input') {
return
}
event.preventDefault()
let items = []
@@ -210,17 +214,13 @@ export default {
if (this.$store.state.clipboard.key === 'x') {
api.move(items).then(() => {
this.$store.commit('setReload', true)
}).catch(error => {
this.$store.commit('showError', error)
})
}).catch(this.$showError)
return
}
api.copy(items).then(() => {
this.$store.commit('setReload', true)
}).catch(error => {
this.$store.commit('showError', error)
})
}).catch(this.$showError)
},
resizeEvent () {
// Update the columns size based on the window width.
@@ -267,7 +267,7 @@ export default {
.then(req => {
this.checkConflict(files, req.items, base)
})
.catch(error => { console.log(error) })
.catch(this.$showError)
return
}
@@ -318,19 +318,37 @@ export default {
handleFiles (files, base, overwrite = false) {
buttons.loading('upload')
let promises = []
let progress = new Array(files.length).fill(0)
for (let file of files) {
promises.push(api.post(this.$route.path + base + file.name, file, overwrite))
let onupload = (id) => (event) => {
progress[id] = (event.loaded / event.total) * 100
let sum = 0
for (let i = 0; i < progress.length; i++) {
sum += progress[i]
}
this.$store.commit('setProgress', Math.ceil(sum / progress.length))
}
for (let i = 0; i < files.length; i++) {
let file = files[i]
promises.push(api.post(this.$route.path + base + file.name, file, overwrite, onupload(i)))
}
let finish = () => {
buttons.success('upload')
this.$store.commit('setProgress', 0)
}
Promise.all(promises)
.then(() => {
buttons.success('upload')
finish()
this.$store.commit('setReload', true)
})
.catch(error => {
buttons.done('upload')
this.$store.commit('showError', error)
finish()
this.$showError(error)
})
return false

View File

@@ -109,21 +109,36 @@ export default {
.then(() => {
this.$store.commit('setReload', true)
})
.catch(error => {
this.$store.commit('showError', error)
})
.catch(this.$showError)
},
click: function (event) {
if (this.selectedCount !== 0) event.preventDefault()
if (this.$store.state.selected.indexOf(this.index) === -1) {
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
} else {
if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index)
return
}
return false
if (event.shiftKey && this.selected.length === 1) {
let fi = 0
let la = 0
if (this.index > this.selected[0]) {
fi = this.selected[0] + 1
la = this.index
} else {
fi = this.index
la = this.selected[0] - 1
}
for (; fi <= la; fi++) {
this.addSelected(fi)
}
return
}
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
},
touchstart (event) {
setTimeout(() => {

View File

@@ -20,8 +20,8 @@
<div class="preview">
<img v-if="req.type == 'image'" :src="raw()">
<audio v-else-if="req.type == 'audio'" :src="raw()" controls></audio>
<video v-else-if="req.type == 'video'" :src="raw()" controls>
<audio v-else-if="req.type == 'audio'" :src="raw()" autoplay controls></audio>
<video v-else-if="req.type == 'video'" :src="raw()" autoplay controls>
Sorry, your browser doesn't support embedded videos,
but don't worry, you can <a :href="download()">download it</a>
and watch it with your favorite video player!
@@ -75,7 +75,7 @@ export default {
this.listing = req
this.updateLinks()
})
.catch(error => { console.log(error) })
.catch(this.$showError)
},
beforeDestroy () {
window.removeEventListener('keyup', this.key)

View File

@@ -1,19 +1,24 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.copy') }}</h3>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.copy') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.copyMessage') }}</p>
<file-list @update:selected="val => dest = val"></file-list>
</div>
<div>
<button class="ok"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
<button class="cancel"
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="copy"
:disabled="$route.path === dest"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
</div>
</div>
</template>
@@ -56,7 +61,7 @@ export default {
})
.catch(error => {
buttons.done('copy')
this.$store.commit('showError', error)
this.$showError(error)
})
}
}

View File

@@ -1,16 +1,18 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.deleteTitle') }}</h3>
<p v-show="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
<p v-show="req.kind === 'listing'">{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
<div>
<button @click="submit"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
<div class="card floating">
<div class="card-content">
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
</div>
<div class="card-action">
<button @click="$store.commit('closeHovers')"
class="flat cancel"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
class="flat"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
</div>
</div>
</template>
@@ -43,7 +45,7 @@ export default {
})
.catch(error => {
buttons.done('delete')
this.$store.commit('showError', error)
this.$showError(error)
})
return
@@ -70,7 +72,7 @@ export default {
.catch(error => {
buttons.done('delete')
this.$store.commit('setReload', true)
this.$store.commit('showError', error)
this.$showError(error)
})
}
}

View File

@@ -1,13 +1,18 @@
<template>
<div class="prompt" id="download">
<h3>{{ $t('prompts.download') }}</h3>
<div class="card floating" id="download">
<div class="card-title">
<h2>{{ $t('prompts.download') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.downloadMessage') }}</p>
<button @click="download('zip')" autofocus>zip</button>
<button @click="download('tar')" autofocus>tar</button>
<button @click="download('targz')" autofocus>tar.gz</button>
<button @click="download('tarbz2')" autofocus>tar.bz2</button>
<button @click="download('tarxz')" autofocus>tar.xz</button>
<button class="block cancel" @click="download('zip')" autofocus>zip</button>
<button class="block cancel" @click="download('tar')" autofocus>tar</button>
<button class="block cancel" @click="download('targz')" autofocus>tar.gz</button>
<button class="block cancel" @click="download('tarbz2')" autofocus>tar.bz2</button>
<button class="block cancel" @click="download('tarxz')" autofocus>tar.xz</button>
</div>
</div>
</template>

View File

@@ -1,31 +0,0 @@
<template>
<div class="prompt error">
<i class="material-icons">error_outline</i>
<h3>{{ $t('prompts.error') }}</h3>
<pre>{{ $store.state.showMessage }}</pre>
<div>
<button @click="close"
autofocus
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
<button @click="reportIssue"
class="cancel"
:aria-label="$t('buttons.reportIssue')"
:title="$t('buttons.reportIssue')">{{ $t('buttons.reportIssue') }}</button>
</div>
</div>
</template>
<script>
export default {
name: 'error',
methods: {
reportIssue () {
window.open('https://github.com/hacdias/filemanager/issues/new')
},
close () {
this.$store.commit('closeHovers')
}
}
}
</script>

View File

@@ -53,7 +53,7 @@ export default {
// so we fetch the data from the previous directory.
api.fetch(url.removeLastDir(this.$route.path))
.then(this.fillOptions)
.catch(this.showError)
.catch(this.$showError)
},
methods: {
fillOptions (req) {
@@ -96,7 +96,7 @@ export default {
api.fetch(uri)
.then(this.fillOptions)
.catch(this.showError)
.catch(this.$showError)
},
touchstart (event) {
let url = event.currentTarget.dataset.url

View File

@@ -1,7 +1,10 @@
<template>
<div class="prompt help">
<h3>{{ $t('help.help') }}</h3>
<div class="card floating help">
<div class="card-title">
<h2>{{ $t('help.help') }}</h2>
</div>
<div class="card-content">
<ul>
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
@@ -13,11 +16,12 @@
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
</ul>
</div>
<div>
<div class="card-action">
<button type="submit"
@click="$store.commit('closeHovers')"
class="ok"
class="flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div>

View File

@@ -1,29 +1,33 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.fileInfo') }}</h3>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.fileInfo') }}</h2>
</div>
<p v-show="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
<div class="card-content">
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
<p v-show="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
<p v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
<p v-show="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
<section v-show="dir() && selected.length === 0">
<template v-if="dir() && selected.length === 0">
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
</section>
</template>
<section v-show="!dir()">
<template v-if="!dir()">
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
</section>
</template>
</div>
<div>
<div class="card-action">
<button type="submit"
@click="$store.commit('closeHovers')"
class="ok"
class="flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div>
@@ -111,7 +115,7 @@ export default {
api.checksum(link, hash)
.then((hash) => { event.target.innerHTML = hash })
.catch(error => { this.$store.commit('showError', error) })
.catch(this.$showError)
}
}
}

View File

@@ -1,19 +1,23 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.move') }}</h3>
<p>{{ $t('prompts.moveMessage') }}</p>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.move') }}</h2>
</div>
<div class="card-content">
<file-list @update:selected="val => dest = val"></file-list>
</div>
<div>
<button class="ok"
@click="move"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
<button class="cancel"
<div class="card-action">
<button class="flat cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
</div>
</div>
</template>
@@ -56,7 +60,7 @@ export default {
})
.catch(error => {
buttons.done('move')
this.$store.commit('showError', error)
this.$showError(error)
})
event.preventDefault()

View File

@@ -1,18 +1,24 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.newFile') }}</h3>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newFile') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newArchetype') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
<div>
<button class="ok"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
<button class="cancel"
</div>
<div class="card-action">
<button class="flat cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
</div>
</div>
</template>
@@ -37,17 +43,19 @@ export default {
.then((url) => {
this.$router.push({ path: url })
})
.catch(error => {
this.$store.commit('showError', error)
})
.catch(this.$showError)
},
new (url, type) {
url = removePrefix(url)
if (!url.endsWith('.md') && !url.endsWith('.markdown')) {
url += '.markdown'
}
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
if (!this.$store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
request.setRequestHeader('Archetype', encodeURIComponent(type))
request.onload = () => {

View File

@@ -1,17 +1,23 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.newDir') }}</h3>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newDir') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newDirMessage') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button class="ok"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
@click="submit">{{ $t('buttons.create') }}</button>
<button class="cancel"
</div>
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
@click="submit">{{ $t('buttons.create') }}</button>
</div>
</div>
</template>
@@ -43,7 +49,7 @@ export default {
api.post(uri)
.then(() => { this.$router.push({ path: uri }) })
.catch(error => { this.$store.commit('showError', error) })
.catch(this.$showError)
// Close the prompt
this.$store.commit('closeHovers')

View File

@@ -1,17 +1,23 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.newFile') }}</h3>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newFile') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newFileMessage') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button class="ok"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
<button class="cancel"
</div>
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
</div>
</div>
</template>
@@ -44,7 +50,7 @@ export default {
// Create the new file.
api.post(uri)
.then(() => { this.$router.push({ path: uri }) })
.catch(error => { this.$store.commit('showError', error) })
.catch(this.$showError)
// Close the prompt.
this.$store.commit('closeHovers')

View File

@@ -9,8 +9,6 @@
<info v-else-if="showInfo"></info>
<move v-else-if="showMove"></move>
<copy v-else-if="showCopy"></copy>
<error v-else-if="showError"></error>
<success v-else-if="showSuccess"></success>
<replace v-else-if="showReplace"></replace>
<schedule v-else-if="show === 'schedule'"></schedule>
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
@@ -27,8 +25,6 @@ import Rename from './Rename'
import Download from './Download'
import Move from './Move'
import Copy from './Copy'
import Error from './Error'
import Success from './Success'
import NewFile from './NewFile'
import NewDir from './NewDir'
import NewArchetype from './NewArchetype'
@@ -47,9 +43,7 @@ export default {
NewArchetype,
Schedule,
Rename,
Error,
Download,
Success,
Move,
Copy,
Share,
@@ -70,8 +64,6 @@ export default {
},
computed: {
...mapState(['show', 'plugins']),
showError: function () { return this.show === 'error' },
showSuccess: function () { return this.show === 'success' },
showInfo: function () { return this.show === 'info' },
showHelp: function () { return this.show === 'help' },
showDelete: function () { return this.show === 'delete' },

View File

@@ -1,18 +1,24 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.rename') }}</h3>
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.rename') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button @click="submit"
type="submit"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
<button class="cancel"
</div>
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
class="flat"
type="submit"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
</div>
</div>
</template>
@@ -68,7 +74,7 @@ export default {
}
this.$store.commit('setReload', true)
}).catch(error => {
this.$store.commit('showError', error)
this.$showError(error)
})
this.$store.commit('closeHovers')

View File

@@ -1,17 +1,22 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.replace') }}</h3>
<p>{{ $t('prompts.replaceMessage') }}</p>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2>
</div>
<div>
<button class="ok"
@click="showConfirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
<button class="cancel"
<div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p>
</div>
<div class="card-action">
<button class="flat cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="showConfirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
</div>
</div>
</template>

View File

@@ -1,17 +1,23 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.schedule') }}</h3>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.schedule') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.scheduleMessage') }}</p>
<input autofocus type="datetime-local" v-model="date">
<div>
<button class="ok"
@click="submit"
:aria-label="$t('buttons.schedule')"
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
<button class="cancel"
</div>
<div class="card-action">
<button class="cancel flat"
@click="close"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="falt"
@click="submit"
:aria-label="$t('buttons.schedule')"
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
</div>
</div>
</template>

View File

@@ -1,7 +1,10 @@
<template>
<div class="prompt" id="share">
<h3>{{ $t('buttons.share') }}</h3>
<p></p>
<div class="card floating" id="share">
<div class="card-title">
<h2>{{ $t('buttons.share') }}</h2>
</div>
<div class="card-content">
<ul>
<li v-if="!hasPermanent">
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
@@ -18,7 +21,7 @@
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
<button class="action copy"
<button class="action copy-clipboard"
:data-clipboard-text="buildLink(link.hash)"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
@@ -43,9 +46,10 @@
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
</li>
</ul>
</div>
<div>
<button class="cancel"
<div class="card-action">
<button class="flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
@@ -54,7 +58,7 @@
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { mapState } from 'vuex'
import { getShare, deleteShare, share } from '@/utils/api'
import moment from 'moment'
import Clipboard from 'clipboard'
@@ -101,20 +105,25 @@ export default {
})
.catch(error => {
if (error === 404) return
this.showError(error)
this.$showError(error)
})
},
mounted () {
this.clip = new Clipboard('.copy')
this.clip = new Clipboard('.copy-clipboard')
this.clip.on('success', (e) => {
this.$showSuccess(this.$t('success.linkCopied'))
})
},
beforeDestroy () {
this.clip.destroy()
},
methods: {
...mapMutations([ 'showError' ]),
submit: function (event) {
if (!this.time) return
share(this.url, this.time, this.unit)
.then(result => { this.links.push(result); this.sort() })
.catch(error => { this.showError(error) })
.catch(this.$showError)
},
getPermalink (event) {
share(this.url)
@@ -123,7 +132,7 @@ export default {
this.sort()
this.hasPermanent = true
})
.catch(error => { this.showError(error) })
.catch(this.$showError)
},
deleteLink (event, link) {
event.preventDefault()
@@ -132,7 +141,7 @@ export default {
if (!link.expires) this.hasPermanent = false
this.links = this.links.filter(item => item.hash !== link.hash)
})
.catch(error => { this.showError(error) })
.catch(this.$showError)
},
humanTime (time) {
return moment(time).fromNow()

View File

@@ -1,23 +0,0 @@
<template>
<div class="prompt success">
<i class="material-icons">done</i>
<h3>{{ $store.state.showMessage }}</h3>
<div>
<button @click="close"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')"
autofocus>{{ $t('buttons.ok') }}</button>
</div>
</div>
</template>
<script>
export default {
name: 'success',
methods: {
close () {
this.$store.commit('closeHovers')
}
}
}
</script>

View File

@@ -1,9 +1,8 @@
body {
font-family: 'Roboto', sans-serif;
padding-top: 4em;
background-color: #f8f8f8;
user-select: none;
color: #212121;
background-color: #fafafa;
color: #333333;
}
* {
@@ -66,6 +65,58 @@ button:hover {
background-color: #1E88E5;
}
input[type="submit"].block,
button.block {
display: block;
width: 100%;
margin: 0 0 1em;
}
button.delete {
background: #F44336;
}
button.delete:hover {
background: #D32F2F;
}
button.cancel {
background-color: #ECEFF1;
color: #37474F;
}
button.cancel:hover {
background-color: #e9eaeb;
}
button.flat,
input[type="submit"].flat {
color: #1E88E5;
background: transparent;
box-shadow: 0 0 0;
border: 0;
margin-left: 0;
text-transform: uppercase;
}
button.flat:hover,
input[type="submit"].flat:hover {
background: rgba(0,0,0,0.05)
}
button.flat.delete {
color: #F44336;
}
button.flat.cancel {
color: #ccc;
}
button.flat[disabled] {
color: #ccc;
cursor: not-allowed;
}
.mobile-only {
display: none !important;
}
@@ -145,3 +196,19 @@ main {
#breadcrumbs span a {
padding: .2em;
}
#progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
z-index: 9999999999;
}
#progress div {
height: 100%;
background-color: #40c4ff;
width: 0;
transition: .2s ease width;
}

View File

@@ -1,64 +1,34 @@
.dashboard {
max-width: 600px;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
border-radius: .5em;
background: #fff;
padding: 1em;
margin: 1em 0;
}
.dashboard a {
a {
color: inherit
}
.dashboard h1 button {
font-size: 0.5em;
float: right;
}
.dashboard table {
width: 100%;
}
.dashboard table th {
font-weight: 500;
color: #757575;
text-align: left;
}
.dashboard table th,
.dashboard table td {
padding: .5em 0;
}
.dashboard table td:last-child {
width: 1em
}
.dashboard > h1:first-of-type {
margin-top: 0;
}
.dashboard form > p:last-child,
form.dashboard > p:last-child {
text-align: right
}
.dashboard > *:last-child {
margin-bottom: 0;
}
.dashboard select,
.dashboard textarea,
.dashboard input[type="text"],
.dashboard input[type="password"] {
padding: 0;
line-height: 1.7;
select,
textarea,
input[type="text"],
input[type="password"] {
padding: 0.5em 0;
line-height: 1;
display: block;
border: 0;
border-bottom: 1px solid #dddddd;
transition: .2s ease border;
width: 100%;
background: transparent;
}
textarea {
line-height: 1.15;
padding: .5em;
border: 1px solid #ddd;
font-family: monospace;
min-height: 10em;
resize: none;
border-radius: 2px;
}
.dashboard #locale,
@@ -69,49 +39,32 @@ form.dashboard > p:last-child {
}
.dashboard #locale {
border: 1px solid #dddddd;
margin-top: .5em;
}
.dashboard textarea:focus,
.dashboard textarea:hover,
.dashboard input[type="text"]:focus,
.dashboard input[type="password"]:focus,
.dashboard input[type="text"]:hover,
.dashboard input[type="password"]:hover {
textarea:focus,
textarea:hover,
input[type="text"]:focus,
input[type="password"]:focus,
input[type="text"]:hover,
input[type="password"]:hover {
border-color: #2979ff;
}
.dashboard input.red {
input.red {
border-color: red;
}
.dashboard input.green {
input.green {
border-color: green;
}
.dashboard button.delete {
background: #F44336;
}
.dashboard button.delete:hover {
background: #D32F2F;
}
.dashboard textarea {
line-height: 1.15;
padding: .5em;
border: 1px solid #ddd;
font-family: monospace;
min-height: 10em;
resize: vertical;
}
.dashboard p label {
margin-bottom: .2em;
display: block;
font-size: .8em;
font-weight: bold;
font-weight: 500;
color: rgba(0, 0, 0, 0.57);
}
li code,
@@ -131,21 +84,337 @@ p code {
display: flex;
color: rgb(84, 110, 122);
font-weight: 500;
padding: 0 0 1em;
margin: 0 0 1em;
font-size: .8em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
text-align: center;
justify-content: space-between;
padding: 0;
}
.dashboard #nav li {
width: 100%;
padding: 0 0 1em;
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
}
.dashboard #nav li:last-child {
text-align: right
.dashboard #nav li.active {
border-color: #2196f3
}
.dashboard #nav i {
font-size: 1em;
vertical-align: middle;
}
table {
border-collapse: collapse;
width: 100%;
}
table tr {
border-bottom: 1px solid #ccc;
}
table tr:last-child {
border: 0;
}
table th {
font-weight: 500;
color: #757575;
text-align: left;
}
table th,
table td {
padding: .5em 0;
}
table td.small {
width: 1em;
}
table tr>*:first-child {
padding-left: 1em;
}
table tr>*:last-child {
padding-right: 1em;
}
.card {
position: relative;
margin: .5rem 0 1rem 0;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
}
.card.floating {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99999;
max-width: 25em;
width: 90%;
max-height: 95%;
z-index: 99999;
animation: .1s show forwards;
}
.card>*>*:first-child {
margin-top: 0;
}
.card>*>*:last-child {
margin-bottom: 0;
}
.card .card-title {
padding: 1.5em 1em 1em;
display: flex;
}
.card .card-title>*:first-child {
margin-right: auto;
}
.card>div {
padding: 1em 1em;
}
.card>div:first-child {
padding-top: 1.5em;
}
.card>div:last-child {
padding-bottom: 1.5em;
}
.card .card-title * {
margin: 0;
}
.card .card-action {
text-align: right;
}
.card .card-content.full {
padding-bottom: 0;
}
.card h2 {
font-weight: 500;
}
.card h3 {
color: rgba(0, 0, 0, 0.53);
font-size: 1em;
font-weight: 500;
margin: 2em 0 1em;
}
.card-content table {
margin: 0 -1em;
width: calc(100% + 2em);
}
.card code {
word-wrap: break-word;
}
.card#download {
max-width: 15em;
}
.card#share ul {
list-style: none;
padding: 0;
margin: 0;
}
.card#share ul li {
display: flex;
justify-content: space-between;
align-items: center;
}
.card#share ul li a {
color: #2196F3;
cursor: pointer;
margin-right: auto;
}
.card#share ul li .action i {
font-size: 1em;
}
.card#share ul li input,
.card#share ul li select {
padding: .2em;
margin-right: .5em;
border: 1px solid #dadada;
}
.card#share .action.copy-clipboard::after {
content: 'Copied!';
position: absolute;
left: -25%;
width: 150%;
font-size: .6em;
text-align: center;
background: #44a6f5;
color: #fff;
padding: .5em .2em;
border-radius: .4em;
top: -2em;
transition: .1s ease opacity;
opacity: 0;
}
.card#share .action.copy-clipboard.active::after {
opacity: 1;
}
.overlay {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 9999;
animation: .1s show forwards;
}
/* * * * * * * * * * * * * * * *
* PROMPT - MOVE *
* * * * * * * * * * * * * * * */
.file-list {
max-height: 50vh;
overflow: auto;
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
.file-list li {
width: 100%;
user-select: none;
border-radius: .2em;
padding: .3em;
}
.file-list li[aria-selected=true] {
background: #2196f3 !important;
color: #fff !important;
transition: .1s ease all;
}
.file-list li:hover {
background-color: #e9eaeb;
cursor: pointer;
}
.file-list li:before {
content: "folder";
color: #6f6f6f;
vertical-align: middle;
line-height: 1.4;
font-family: 'Material Icons';
font-size: 1.75em;
margin-right: .25em;
}
.file-list li[aria-selected=true]:before {
color: white;
}
.help {
max-width: 24em;
}
.help ul {
padding: 0;
margin: 1em 0;
list-style: none;
}
@keyframes show {
0% {
display: none;
opacity: 0;
}
1% {
display: block;
opacity: 0;
}
100% {
display: block;
opacity: 1;
}
}
.collapsible {
border-top: 1px solid rgba(0,0,0,0.1);
}
.collapsible:last-of-type {
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.collapsible > input {
display: none;
}
.collapsible > label {
padding: 1em 0;
cursor: pointer;
border-right: 0;
border-left: 0;
display: flex;
justify-content: space-between;
}
.collapsible > label * {
margin: 0;
color: rgba(0,0,0,0.57);
}
.collapsible > label i {
transition: .2s ease transform;
user-select: none;
}
.collapsible .collapse {
max-height: 0;
overflow: hidden;
transition: .2s ease all;
}
.collapsible > input:checked ~ .collapse {
padding-top: 1em;
padding-bottom: 1em;
max-height: 20em;
}
.collapsible > input:checked ~ label i {
transform: rotate(180deg)
}
.card .collapsible {
width: calc(100% + 2em);
margin: 0 -1em;
}
.card .collapsible > label {
padding: 1em;
}
.card .collapsible .collapse {
padding: 0 1em;
}

View File

@@ -122,8 +122,9 @@ header .search-button {
#search input {
width: 100%;
border: 0;
outline: 0;
background-color: transparent;
line-height: 0;
padding: 0;
}
#search #result {

View File

@@ -159,7 +159,7 @@
#listing.list .item.header {
display: flex !important;
background: #f8f8f8;
background: #fafafa;
position: fixed;
width: calc(100% - 19em);
top: 7em;

View File

@@ -29,6 +29,14 @@
width: 90%;
}
#login.recaptcha form {
min-width: 304px;
}
#login #recaptcha {
margin: .5em 0 0;
}
#login input {
width: 100%;
width: 100%;

View File

@@ -1,208 +0,0 @@
.prompt {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
padding: 2em;
max-width: 25em;
width: 90%;
max-height: 95%;
z-index: 99999;
animation: .1s show forwards;
}
.overlay {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 9999;
animation: .1s show forwards;
}
.prompt h3 {
margin: 0;
font-weight: 500;
font-size: 1.5em;
}
.prompt p {
font-size: .9em;
color: rgba(0, 0, 0, 0.8);
margin: .5em 0 1em;
}
.prompt input:not([type="submit"]) {
width: 100%;
border: 1px solid #dadada;
line-height: 1;
padding: .3em;
margin: .3em 0;
}
.prompt code {
word-wrap: break-word;
}
.prompt div:last-of-type {
margin-top: 1em;
display: flex;
justify-content: flex-start;
flex-direction: row-reverse;
}
.prompt .cancel {
background-color: #ECEFF1;
color: #37474F;
}
.prompt .cancel:hover {
background-color: #e9eaeb;
}
.prompt.success i,
.prompt.error i {
color: #F44336;
display: block;
margin: 0 auto .15em;
text-align: center;
font-size: 5em;
}
.prompt.success h3,
.prompt.error h3 {
text-align: center;
}
.prompt.error button:not(.cancel) {
background-color: #F44336
}
.prompt.success i {
color: #8BC34A;
}
.prompt.success button {
background-color: #8BC34A;
}
/* * * * * * * * * * * * * * * *
* PROMPT - MOVE *
* * * * * * * * * * * * * * * */
.file-list {
max-height: 50vh;
overflow: auto;
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
.file-list li {
width: 100%;
user-select: none;
border-radius: .2em;
padding: .3em;
}
.file-list li[aria-selected=true] {
background: #2196f3 !important;
color: #fff !important;
transition: .1s ease all;
}
.file-list li:hover {
background-color: #e9eaeb;
cursor: pointer;
}
.file-list li:before {
content: "folder";
color: #6f6f6f;
vertical-align: middle;
line-height: 1.4;
font-family: 'Material Icons';
font-size: 1.75em;
margin-right: .25em;
}
.file-list li[aria-selected=true]:before {
color: white;
}
.prompt#download {
max-width: 15em;
}
.prompt#download button {
width: 100%;
display: block;
margin: 0 0 1em;
background-color: #ECEFF1;
color: #37474F;
}
.prompt#download button:last-of-type {
margin-bottom: 0;
}
.help {
max-width: 24em;
}
.help ul {
padding: 0;
margin: 1em 0;
list-style: none;
}
@keyframes show {
0% {
display: none;
opacity: 0;
}
1% {
display: block;
opacity: 0;
}
100% {
display: block;
opacity: 1;
}
}
.prompt#share ul {
list-style: none;
padding: 0;
margin: 0;
}
.prompt#share ul li {
display: flex;
justify-content: space-between;
align-items: center;
}
.prompt#share ul li a {
color: #2196F3;
cursor: pointer;
margin-right: auto;
}
.prompt#share ul li .action i {
font-size: 1em;
}
.prompt#share ul li input,
.prompt#share ul li select {
padding: .2em;
margin-right: .5em;
border: 1px solid #dadada;
}

View File

@@ -1,8 +1,8 @@
@import "~normalize.css/normalize.css";
@import "~noty/lib/noty.css";
@import "./fonts.css";
@import "./base.css";
@import "./header.css";
@import "./prompts.css";
@import "./listing.css";
@import "./editor.css";
@import "./dashboard.css";
@@ -180,6 +180,17 @@
* PROMPT *
* * * * * * * * * * * * * * * */
.noty_buttons {
text-align: right;
padding: 0 10px 10px !important;
}
.noty_buttons button {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0,0,0,0.1);
box-shadow: 0 0 0 0;
font-size: 14px;
}
/* * * * * * * * * * * * * * * *
* FOOTER *

View File

@@ -26,11 +26,13 @@ buttons:
publish: Publish
selectMultiple: Select multiple
schedule: Schedule
switchView: Swicth view
switchView: Switch view
toggleSidebar: Toggle sidebar
update: Update
upload: Upload
permalink: Get Permanent Link
success:
linkCopied: Link copied!
errors:
forbidden: You're not welcome here.
internal: Something really went wrong.
@@ -115,13 +117,14 @@ settings:
commandsHelp: >
Here you can set commands that are executed in the named events. You
write one command per line. If the event is related to files, such as before and
after saving, the environment variable "file" will be available with the path
after saving, the environment variable "FILE" will be available with the path
of the file.
commandsUpdated: Commands updated!
customStylesheet: Custom Stylesheet
examples: Examples
globalSettings: Global Settings
language: Language
lockPassword: Prevent the user from changing the password
newPassword: Your new password
newPasswordConfirm: Confirm your new password
newUser: New User
@@ -165,7 +168,6 @@ sidebar:
myFiles: My files
newFile: New file
newFolder: New folder
servedWith: Served with
settings: Settings
siteSettings: Site Settings
hugoNew: Hugo New
@@ -185,10 +187,12 @@ search:
writeToSearch: Write here to search
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
es: Español
time:
unit: Time Unit
seconds: Seconds

202
assets/src/i18n/es.yaml Normal file
View File

@@ -0,0 +1,202 @@
permanent: Permanente
buttons:
cancel: Cancelar
close: Cerrar
copy: Copiar
copyFile: Copiar archivo
copyToClipboard: Copiar al portapapeles
create: Crear
delete: Borrar
download: Descargar
info: Info
more: Más
move: Mover
moveFile: Mover archivo
new: Nuevo
next: Siguiente
ok: OK
replace: Reemplazar
previous: Anterior
rename: Renombrar
reportIssue: Reportar problema
save: Guardar
search: Buscar
select: Seleccionar
share: Compartir
publish: Publicar
selectMultiple: Selección múltiple
schedule: Programar
switchView: Cambiar vista
toggleSidebar: Mostrar/Ocultar menú
update: Actualizar
upload: Subir
permalink: Link permanente
success:
linkCopied: ¡Link copiado!
errors:
forbidden: No eres bienvenido aquí.
internal: La verdad es que algo ha ido mal.
notFound: No se puede acceder a este lugar.
files:
folders: Carpetas
files: Archivos
body: Cuerpo
clear: Limpiar
closePreview: Cerrar vista previa
home: Inicio
lastModified: Última modificación
loading: Cargando...
lonely: Uno se siente muy sólo aquí...
metadata: Metadatos
multipleSelectionEnabled: Selección múltiple activada
name: Nombre
size: Tamaño
sortByName: Ordenar por nombre
sortBySize: Ordenar por tamaño
sortByLastModified: Ordenar por última modificación
help:
click: seleccionar archivo o carpeta
ctrl:
click: seleccionar múltiples archivos o carpetas
f: abre la búsqueda
s: guarda un archivo o lo descarga a la carpeta en la que estás
del: elimina los items seleccionados
doubleClick: abre un archivo o carpeta
esc: limpia la selección y/o cierra la ventana
f1: esta información
f2: renombrar archivo
help: Ayuda
login:
password: Contraseña
submit: Iniciar sesión
username: Usuario
wrongCredentials: Usuario y/o contraseña incorrectos
prompts:
copy: Copiar
copyMessage: 'Elige el lugar donde quieres copiar tus archivos:'
currentlyNavigating: 'Actualmente estás en:'
deleteMessageMultiple: ¿Estás seguro que quieres eliminar {count} archivo(s)?
deleteMessageSingle: ¿Estás seguro que quieres eliminar este archivo/carpeta?
deleteTitle: Borrar archivos
displayName: 'Nombre:'
download: Descargar archivos
downloadMessage: Elige el formato de descarga.
error: Algo ha fallado
fileInfo: Información del archivo
filesSelected: "{count} archivos seleccionados."
lastModified: Última modificación
move: Mover
moveMessage: 'Elige una nueva casa para tus archivo(s)/carpeta(s):'
newDir: Nueva carpeta
newDirMessage: Escribe el nombre de la nueva carpeta.
newFile: Nuevo archivo
newFileMessage: Escribe el nombre del nuevo archivo.
numberDirs: Número de carpetas
numberFiles: Número de archivos
replace: Reemplazar
replaceMessage: >
Uno de los archivos ue intentas subir está creando conflicto por su nombre.
¿Quieres cambiar el nombre del ya existente?
rename: Renombrar
renameMessage: Escribe el nuevo nombre para
show: Mostrar
size: Tamaño
schedule: Programar
scheduleMessage: Elige una hora y fecha para programar la publicación de este post.
newArchetype: Crea un nuevo post basado en un arquetipo. Tu archivo será creado en la carpeta de contenido.
settings:
admin: Admin
administrator: Administrador
allowCommands: Ejecutar comandos
allowEdit: Editar, renombrar y borrar archivos o carpetas
allowNew: Crear nuevos archivos y carpetas
allowPublish: Publicar nuevos posts y páginas
avoidChanges: "(dejar en blanco para evitar cambios)"
changePassword: Cambiar contraseña
commands: Comandos
commandsHelp: >
Aquí puedes crear comandos que serán ejecutados en los eventos. Debes
escribir un comando por linea. Si el evento está relacionado con archivos, como
por ejemplo, antes y después de guardar, la variable de entorno "FILE" estará
disponible en la ruta del archivo.
commandsUpdated: ¡Comandos actualizados!
customStylesheet: Modificar hoja de estilos
examples: Ejemplos
globalSettings: Ajustes globales
language: Idioma
lockPassword: Evitar que el usuario cambie la contraseña
newPassword: Tu nueva contraseña
newPasswordConfirm: Confirma tu contraseña
newUser: Nuevo usuario
password: Contraseña
passwordUpdated: ¡Contraseña actualizada!
permissions: Permisos
permissionsHelp: >
Puedes nombrar al usuario como administrador o elegir los permisos
individualmente. Si seleccionas "Administrador", todas las otras opciones
serán activadas automáticamente. La administración de usuarios es un privilegio de administrador.
profileSettings: Ajustes del perfil
ruleExample1: >
previene el acceso a una extensión de archivo (Como .git) en
cada carpeta.
ruleExample2: bloquea el acceso al archivo llamado Caddyfile en la carpeta raíz.
rules: Reglas
rulesHelp1: >
Aquí puedes definir un conjunto de reglas de permisos para este usuario
específico. Los archivos bloqueados no se mostrarán en las listas y no serán accesibles
por el usuario. Puedes utilizar regex y rutas relativas a la raíz del usuario.
rulesHelp2: >
Cada regla va en una línea diferente, y debe comenzar con la palabra clave
{0} or {1}. Entonces, debes escribir {2} si estás usando una expresión regular (REGEX) y
luego la expresión o la ruta.
scope: Raíz
settingsUpdated: ¡Ajustes actualizados!
user: Usuario
userCommands: Comandos
userCommandsHelp: >
Una lista separada por espacios con los comandos permitidos para este usuario.
Ejemplo:
userCreated: ¡Usuario creado!
userDeleted: ¡Usuario eliminado!
userManagement: Administración de usuarios
username: Usuario
users: Usuarios
userUpdated: ¡Usuario actualizado!
sidebar:
help: Ayuda
logout: Cerrar sesión
myFiles: Mis archivos
newFile: Nuevo archivo
newFolder: Nueva carpeta
settings: Ajustes
siteSettings: Ajustes del sitio
hugoNew: Nuevo Hugo
preview: Vista previa
search:
images: Images
music: Música
pdf: PDF
pressToExecute: Presiona enter para ejecutar.
pressToSearch: Presiona enter para buscar.
search: Buscar...
searchOrCommand: Buscar o ejecutar un comando...
searchOrSupportedCommand: 'Buscar o ejecutar uno de los comandos soportados:'
type: Escribe y presiona enter para buscar.
types: Tipos
video: Vídeo
writeToSearch: Escribe aquí para buscar
languages:
en: English
fr: Français
pt: Português
es: Español
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time:
unit: Unidad
seconds: Segundos
minutes: Minutos
hours: Horas
days: Días

194
assets/src/i18n/fr.yaml Normal file
View File

@@ -0,0 +1,194 @@
permanent: Permanent
buttons:
cancel: Annuler
close: Fermer
copy: Copier
copyFile: Copier le fichier
copyToClipboard: Copier dans le presse-papier
create: Créer
delete: Supprimer
download: Télécharger
info: Info
more: Plus
move: Déplacer
moveFile: Déplacer le fichier
new: Nouveau
next: Suivant
ok: OK
replace: Remplacer
previous: Précédent
rename: Renommer
reportIssue: Rapport d'erreur
save: Enregistrer
search: Chercher
select: Sélectionner
share: Partager
publish: Publier
selectMultiple: Sélection multiple
schedule: Fixer la date
switchView: Changer le mode d'affichage
toggleSidebar: Afficher/Masquer la barre latérale
update: Mettre à jour
upload: Importer
permalink: Obtenir un lien permanent
errors:
forbidden: Vous n'êtes pas autorisé à être ici.
internal: Aïe ! Quelque chose s'est mal passé.
notFound: Impossible d'accéder à cet emplacement.
files:
folders: Dossiers
files: Fichiers
body: Corps
clear: Fermer
closePreview: Fermer la prévisualisation
home: Accueil
lastModified: Dernière modification
loading: Chargement...
lonely: Il semble qu'il n'y ai rien par ici...
metadata: Metadonnées
multipleSelectionEnabled: Sélection multiple activée
name: Nom
size: Taille
sortByName: Trier par nom
sortBySize: Trier par taille
sortByLastModified: Trier par date de dernière modification
help:
click: Sélectionner un élément
ctrl:
click: Sélectionner plusieurs éléments
f: Ouvrir l'invité de recherche
s: Télécharger l'élément actuel
del: Supprimer les éléments sélectionnés
doubleClick: Ouvrir un élément
esc: Désélectionner et/ou fermer la boîte de dialogue
f1: Ouvrir l'aide
f2: Renommer le fichier
help: Aide
login:
password: Mot de passe
submit: Se connecter
username: Utilisateur
wrongCredentials: Identifiants incorrects !
prompts:
copy: Copier
copyMessage: 'Choisissez l''emplacement où copier la sélection :'
currentlyNavigating: 'Dossier courant :'
deleteMessageMultiple: Etes-vous sûr de vouloir supprimer ces {count} élément(s) ?
deleteMessageSingle: Etes-vous sûr de vouloir supprimer cet élément ?
deleteTitle: Supprimer
displayName: 'Nom :'
download: Télécharger
downloadMessage: 'Choisissez le format de téléchargement :'
error: Quelque chose s'est mal passé
fileInfo: Informations
filesSelected: "{count} éléments sélectionnés"
lastModified: Dernière modification
move: Déplacer
moveMessage: 'Choisissez l''emplacement où déplacer la sélection :'
newDir: Nouveau dossier
newDirMessage: 'Nom du nouveau dossier :'
newFile: Nouveau fichier
newFileMessage: 'Nom du nouveau fichier :'
numberDirs: Nombre de dossiers
numberFiles: Nombre de fichiers
replace: Remplacer
replaceMessage: >
Un des fichiers que vous êtes en train d'importer a le même nom qu'un autre déjà présent.
Voulez-vous remplacer le fichier actuel par le nouveau ?
rename: Renommer
renameMessage: Nouveau nom pour
show: Montrer
size: Taille
schedule: Fixer la date
scheduleMessage: Choisissez une date pour planifier la publication de ce post
newArchetype: Créer un nouveau post basé sur un archétype. Votre fichier sera créé dans le dossier de contenu.
settings:
admin: Admin
administrator: Administrateur
allowCommands: Exécuter des commandes
allowEdit: Editer, renommer et supprimer des fichiers ou des dossiers
allowNew: Créer de nouveaux fichiers et dossiers
allowPublish: Publier de nouveaux posts et pages
avoidChanges: "(Laisser vide pour conserver l'actuel)"
changePassword: Modifier le mot de passe
commands: Commandes
commandsHelp: >
Ici vous pouvez définir des commandes qui seront exécutées lors de l'évènement correspondant.
Vous devez indiquer une commande par ligne. Si l'évènement est en rapport avec des fichiers,
par exemple avant et après enregistrement, la variable d'environement "FILE" sera disponible
et contiendra le chemin d'accès vers le fichier.
commandsUpdated: Commandes mises à jour !
customStylesheet: Feuille de style personnalisée
examples: Exemples
globalSettings: Paramètres généraux
language: Langue
newPassword: Votre nouveau mot de passe
newPasswordConfirm: Confirmation du nouveau mot de passe
newUser: Nouvel Utilisateur
password: Mot de passe
passwordUpdated: Mot de passe mis à jour !
permissions: Permissions
permissionsHelp: >
Vous pouvez définir l'utilisateur comme étant un administrateur ou encore choisir les
permissions individuellement. Si vous sélectionnez "Administrateur", toutes les autres
options seront automatiquement activées. La gestion des utilisateurs est un privilège que
seul l'administrateur possède.
profileSettings: Paramètres du profil
ruleExample1: Bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers
ruleExample2: Bloque l'accès au fichier nommé "Caddyfile" à la racine du dossier utilisateur
rules: Règles
rulesHelp1: >
Vous pouvez définir ici un ensemble de règles pour cet utilisateur.
Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur.
Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur.
rulesHelp2: >
Chaque règle est définie sur une ligne différente et doit commencer par le mot clé {0} ou {1}.
Vous devez ensuite ajouter {2} si vous utilisez une expression régulière puis l'expression en question ou bien seulement le chemin d'accès.
scope: Portée du dossier utilisateur
settingsUpdated: Les paramètres ont été mis à jour !
user: Utilisateur
userCommands: Commandes
userCommandsHelp: 'Une liste séparée par des espaces des commandes permises pour l''utilisateur. Exemple :'
userCreated: Utilisateur créé !
userDeleted: Utilisateur supprimé !
userManagement: Gestion des utilisateurs
username: Nom d'utilisateur
users: Utilisateurs
userUpdated: Utilisateur mis à jour !
sidebar:
help: Aide
logout: Se déconnecter
myFiles: Mes fichiers
newFile: Nouveau fichier
newFolder: Nouveau dossier
settings: Paramètres
siteSettings: Paramètres du site
hugoNew: Nouveau Hugo
preview: Prévisualiser
search:
images: Images
music: Musique
pdf: PDF
pressToExecute: Appuyez sur Entrée pour exécuter
pressToSearch: Appuyez sur Entrée pour lancer la recherche
search: Recherche en cours...
searchOrCommand: Rechercher ou exécuter une commande...
searchOrSupportedCommand: 'Lancez une recherche ou exécutez une commande parmis les suivantes :'
type: Tapez votre recherche et appuyez sur Entrée
types: Types
video: Video
writeToSearch: Ecrivez ici pour lancer une recherche
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
es: Español
time:
unit: Unité de temps
seconds: Secondes
minutes: Minutes
hours: Heures
days: Jours

View File

@@ -1,22 +1,60 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import en from './en.yaml'
import fr from './fr.yaml'
import pt from './pt.yaml'
import ja from './ja.yaml'
import zhCN from './zh-cn.yaml'
import zhTW from './zh-tw.yaml'
import es from './es.yaml'
Vue.use(VueI18n)
export function detectLocale () {
let locale = (navigator.language || navigator.browserLangugae).toLowerCase()
switch (true) {
case /^en.*/i.test(locale):
locale = 'en'
break
case /^fr.*/i.test(locale):
locale = 'fr'
break
case /^pt.*/i.test(locale):
locale = 'pt'
break
case /^ja.*/i.test(locale):
locale = 'ja'
break
case /^zh-CN/i.test(locale):
locale = 'zh-cn'
break
case /^zh-TW/i.test(locale):
locale = 'zh-tw'
break
case /^zh.*/i.test(locale):
locale = 'zh-cn'
break
case /^es.*/i.test(locale):
locale = 'es'
break
default:
locale = 'en'
}
return locale
}
const i18n = new VueI18n({
locale: 'en',
locale: detectLocale(),
fallbackLocale: 'en',
messages: {
'en': en,
'fr': fr,
'pt': pt,
'ja': ja,
'zh-cn': zhCN,
'zh-tw': zhTW
'zh-tw': zhTW,
'es': es
}
})

View File

@@ -31,6 +31,8 @@ buttons:
update: 更新
upload: アップロード
permalink: 固定リンク
success:
linkCopied: リンクがコピーされました!
errors:
forbidden: アクセスが拒否されました。
internal: 内部エラーが発生しました。
@@ -116,12 +118,13 @@ settings:
ここで、名前付きイベントに実行するコマンドを設定することができます。\
一行にコマンド一つを入力してください。\
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\
環境変数 file はファイルのパスに割り当てられます。"
環境変数 FILE はファイルのパスに割り当てられます。"
commandsUpdated: コマンドは更新されました!
customStylesheet: カスタムスタイルシ ート
examples:
globalSettings: グローバル設定
language: 言語
lockPassword: 新しいパスワードを変更に禁止
newPassword: 新しいパスワード
newPasswordConfirm: 新しいパスワードを確認します
newUser: 新しいユーザー
@@ -165,7 +168,6 @@ sidebar:
myFiles: 私のファイル
newFile: 新しいファイルを作成
newFolder: 新しいフォルダを作成
servedWith: サービス提供者
settings: 設定
siteSettings: サイト設定
hugoNew: Hugo New
@@ -185,10 +187,12 @@ search:
writeToSearch: ここにキーワードを入力してください
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
es: Español
time:
unit: 時間単位
seconds:

View File

@@ -31,6 +31,8 @@ buttons:
update: Atualizar
upload: Enviar
permalink: Obter link permanente
success:
linkCopied: Link copiado!
errors:
forbidden: Tu não és bem-vindo aqui.
internal: Algo correu bastante mal.
@@ -66,10 +68,12 @@ help:
help: Ajuda
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
es: Español
login:
password: Palavra-passe
submit: Login
@@ -136,12 +140,13 @@ settings:
Pode definir um conjunto de comandos a executar em determiandos eventos.
Deve escrever um comando por linha. Se o evento estiver relacionado com ficheiros,
como antes e depois de guardar, irá existir uma variável de ambiente denominada
"file" com o caminho do ficheiro.
"FILE" com o caminho do ficheiro.
commandsUpdated: Comandos atualizados!
customStylesheet: Estilos Personalizados
examples: Exemplos
globalSettings: Configurações Globais
language: Linguagem
lockPassword: Não permitir que o utilizador altere a palavra-passe
newPassword: Nova palavra-passe
newPasswordConfirm: Confirme a nova palavra-passe
newUser: Novo Utilizador
@@ -189,7 +194,6 @@ sidebar:
newFile: Novo ficheiro
newFolder: Nova pasta
preview: Pré-visualizar
servedWith: Servido com
settings: Configurações
siteSettings: Configurações do Site
time:

View File

@@ -31,6 +31,8 @@ buttons:
update: 更新
upload: 上传
permalink: 获取永久链接
success:
linkCopied: 链接已复制!
errors:
forbidden: 你被禁止访问。
internal: 内部出现麻烦了。
@@ -115,12 +117,13 @@ settings:
commandsHelp: "\
在这里,您可以设置在指定事件下执行的命令,一行一条。\
若事件与文件相关,如“在保存文件前”,\
则文件的路径会被赋值给环境变量 \"file\"。"
则文件的路径会被赋值给环境变量 \"FILE\"。"
commandsUpdated: 命令已更新!
customStylesheet: 自定义样式表
examples: 例子
globalSettings: 全局设置
language: 语言
lockPassword: 禁止用户修改密码
newPassword: 您的新密码
newPasswordConfirm: 重输一遍新密码
newUser: 新建用户
@@ -163,7 +166,6 @@ sidebar:
myFiles: 我的文件
newFile: 新建文件
newFolder: 新建文件夹
servedWith: '服务提供者:'
settings: 设置
siteSettings: 网站设置
hugoNew: Hugo New
@@ -183,10 +185,12 @@ search:
writeToSearch: 请输入要搜索的内容
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
es: Español
time:
unit: 时间单位
seconds:

View File

@@ -4,7 +4,7 @@ buttons:
close: 關閉
copy: 複製
copyFile: 複製檔案
copyToClipboard: 複製到剪貼
copyToClipboard: 複製到剪貼簿
create: 建立
delete: 刪除
download: 下載
@@ -31,8 +31,10 @@ buttons:
update: 更新
upload: 上傳
permalink: 獲取永久連結
success:
linkCopied: 連結已複製!
errors:
forbidden: 你被禁止訪問
forbidden: 你被禁止存取
internal: 內部出現麻煩了。
notFound: 找不到檔案。
files:
@@ -45,7 +47,7 @@ files:
lastModified: 最後修改
loading: 讀取中...
lonely: 這裡沒有任何檔案...
metadata: 中繼資料
metadata: 詮釋資料
multipleSelectionEnabled: 多選模式已開啟
name: 名稱
size: 大小
@@ -100,70 +102,70 @@ prompts:
show: 顯示
size: 大小
schedule: 計畫
scheduleMessage: 請選擇發佈這篇帖子的日期。
newArchetype: 建立一個基於原型的新帖子。您的檔案將會建立在內容資料夾中。
scheduleMessage: 請選擇發佈這篇貼文的日期。
newArchetype: 建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。
settings:
admin: 管理員
administrator: 管理員
allowCommands: 執行命令
allowEdit: 編輯、重命名或刪除檔案/目錄
allowNew: 創建新檔案和目錄
allowPublish: 發佈新的帖子與頁面
allowPublish: 發佈新的貼文與頁面
avoidChanges: '(留空以避免更改)'
changePassword: 更改密碼
commands: 命令
commandsHelp: "\
在這裡,您可以設定在指定事件下執行的命令,一行一條。\
若事件與檔案相關,如“在保存檔案前”,\
則檔案的路徑會被賦值給環境變數 \"file\"。"
則檔案的路徑會被賦值給環境變數 \"FILE\"。"
commandsUpdated: 命令已更新!
customStylesheet: 自定義樣式表
examples:
examples:
globalSettings: 全域設定
language: 語言
lockPassword: 禁止使用者修改密碼
newPassword: 您的新密碼
newPasswordConfirm: 重輸一遍新密碼
newUser: 建立用戶
newUser: 建立使用者
password: 密碼
passwordUpdated: 密碼已更新!
permissions: 權限
permissionsHelp: "\
您可以將該用戶設置為管理員,也可以單獨選擇各項權限。\
您可以將該使用者設置為管理員,也可以單獨選擇各項權限。\
如果選擇了“管理員”,則其他的選項會被自動勾上,\
同時該用戶可以管理其他用戶。"
同時該使用者可以管理其他使用者。"
profileSettings: 設定檔設定
ruleExample1: "\
封鎖用戶訪問所有資料夾下任何以 . 開頭的檔案\
封鎖使用者存取所有資料夾下任何以 . 開頭的檔案\
(隱藏文件, 例如: .git, .gitignore)。"
ruleExample2: 封鎖用戶訪問其目錄範圍的根目錄下名為 Caddyfile 的檔案。
ruleExample2: 封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。
rules: 規則
rulesHelp1: "\
您可以為該用戶製定一組黑名單或白名單式的規則,\
被屏蔽的檔案將不會顯示在清單中,用戶也無權限訪問,\
您可以為該使用者製定一組黑名單或白名單式的規則,\
被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,\
支持相對於目錄範圍的路徑。"
rulesHelp2: "\
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
scope: 目錄範圍
settingsUpdated: 設定已更新!
user: 用戶
userCommands: 用戶命令
user: 使用者
userCommands: 使用者命令
userCommandsHelp: "\
指定該用戶可以執行的命令,用空格分隔。\
指定該使用者可以執行的命令,用空格分隔。\
例如:"
userCreated: 用戶已建立!
userDeleted: 用戶已刪除!
userManagement: 用戶管理
username: 用戶名
users: 用戶
userUpdated: 用戶已更新!
userCreated: 使用者已建立!
userDeleted: 使用者已刪除!
userManagement: 使用者管理
username: 使用者名稱
users: 使用者
userUpdated: 使用者已更新!
sidebar:
help: 幫助
logout: 登出
myFiles: 我的檔案
newFile: 建立檔案
newFolder: 建立資料夾
servedWith: '服務提供者:'
settings: 設定
siteSettings: 網站設定
hugoNew: Hugo New
@@ -183,9 +185,12 @@ search:
writeToSearch: 請輸入要搜尋的內容
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
es: Español
time:
unit: 時間單位
seconds:

View File

@@ -3,9 +3,46 @@ import App from './App'
import store from './store'
import router from './router'
import i18n from './i18n'
import Noty from 'noty'
Vue.config.productionTip = true
const notyDefault = {
type: 'info',
layout: 'bottomRight',
timeout: 1000,
progressBar: true
}
Vue.prototype.$noty = function (opts) {
new Noty(Object.assign({}, notyDefault, opts)).show()
}
Vue.prototype.$showSuccess = function (message) {
new Noty(Object.assign({}, notyDefault, {
text: message,
type: 'success'
})).show()
}
Vue.prototype.$showError = function (error) {
let n = new Noty(Object.assign({}, notyDefault, {
text: error,
type: 'error',
timeout: null,
buttons: [
Noty.button(i18n.t('buttons.reportIssue'), '', function () {
window.open('https://github.com/hacdias/filemanager/issues/new')
}),
Noty.button(i18n.t('buttons.close'), '', function () {
n.close()
})
]
}))
n.show()
}
/* eslint-disable no-new */
new Vue({
el: '#app',

View File

@@ -3,14 +3,15 @@ import Router from 'vue-router'
import Login from '@/views/Login'
import Layout from '@/views/Layout'
import Files from '@/views/Files'
import Users from '@/views/Users'
import User from '@/views/User'
import GlobalSettings from '@/views/GlobalSettings'
import ProfileSettings from '@/views/ProfileSettings'
import Users from '@/views/settings/Users'
import User from '@/views/settings/User'
import Settings from '@/views/Settings'
import GlobalSettings from '@/views/settings/Global'
import ProfileSettings from '@/views/settings/Profile'
import Error403 from '@/views/errors/403'
import Error404 from '@/views/errors/404'
import Error500 from '@/views/errors/500'
import auth from '@/utils/auth.js'
import auth from '@/utils/auth'
import store from '@/store'
Vue.use(Router)
@@ -49,10 +50,14 @@ const router = new Router({
{
path: '/settings',
name: 'Settings',
component: Settings,
redirect: {
path: '/settings/profile'
}
},
meta: {
disableOnNoAuth: true
},
children: [
{
path: '/settings/profile',
name: 'Profile Settings',
@@ -66,6 +71,24 @@ const router = new Router({
requiresAdmin: true
}
},
{
path: '/settings/users',
name: 'Users',
component: Users,
meta: {
requiresAdmin: true
}
},
{
path: '/settings/users/*',
name: 'User',
component: User,
meta: {
requiresAdmin: true
}
}
]
},
{
path: '/403',
name: 'Forbidden',
@@ -81,22 +104,6 @@ const router = new Router({
name: 'Internal Server Error',
component: Error500
},
{
path: '/users',
name: 'Users',
component: Users,
meta: {
requiresAdmin: true
}
},
{
path: '/users/*',
name: 'User',
component: User,
meta: {
requiresAdmin: true
}
},
{
path: '/files',
redirect: {
@@ -123,17 +130,18 @@ router.beforeEach((to, from, next) => {
auth.loggedIn()
.then(() => {
if (to.matched.some(record => record.meta.requiresAdmin)) {
if (store.state.user.admin) {
next()
if (!store.state.user.admin) {
next({ path: '/403' })
return
}
}
next({
path: '/403'
})
if (to.matched.some(record => record.meta.disableOnNoAuth)) {
if (store.state.noAuth) {
next({ path: '/403' })
return
}
}
next()
})

View File

@@ -12,9 +12,18 @@ const state = {
key: '',
items: []
},
css: (() => {
let css = window.CSS
window.CSS = null
return css
})(),
recaptcha: document.querySelector('meta[name="recaptcha"]').getAttribute('content'),
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'),
version: document.querySelector('meta[name="version"]').getAttribute('content'),
jwt: '',
progress: 0,
schedule: '',
loading: false,
reload: false,

View File

@@ -1,4 +1,4 @@
import i18n from '@/i18n'
import * as i18n from '@/i18n'
import moment from 'moment'
const mutations = {
@@ -27,10 +27,17 @@ const mutations = {
setLoading: (state, value) => { state.loading = value },
setReload: (state, value) => { state.reload = value },
setUser: (state, value) => {
moment.locale(value.locale)
i18n.locale = value.locale
let locale = value.locale
if (locale === '') {
locale = i18n.detectLocale()
}
moment.locale(locale)
i18n.default.locale = locale
state.user = value
},
setCSS: (state, value) => (state.css = value),
setJWT: (state, value) => (state.jwt = value),
multiple: (state, value) => (state.multiple = value),
addSelected: (state, value) => (state.selected.push(value)),
@@ -45,8 +52,12 @@ const mutations = {
resetSelected: (state) => {
state.selected = []
},
listingDisplay: (state, value) => {
state.req.display = value
updateUser: (state, value) => {
if (typeof value !== 'object') return
for (let field in value) {
state.user[field] = value[field]
}
},
updateRequest: (state, value) => {
state.req = value
@@ -61,6 +72,9 @@ const mutations = {
},
setSchedule: (state, value) => {
state.schedule = value
},
setProgress: (state, value) => {
state.progress = value
}
}

View File

@@ -18,7 +18,7 @@ export function fetch (url) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -41,7 +41,7 @@ export function remove (url) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
@@ -56,13 +56,17 @@ export function remove (url) {
})
}
export function post (url, content = '', overwrite = false) {
export function post (url, content = '', overwrite = false, onupload) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (typeof onupload === 'function') {
request.upload.onprogress = onupload
}
if (overwrite) {
request.setRequestHeader('Action', `override`)
@@ -91,7 +95,7 @@ export function put (url, content = '', publish = false, date = '') {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.setRequestHeader('Publish', publish)
if (date !== '') {
@@ -121,7 +125,7 @@ function moveCopy (items, copy = false) {
promises.push(new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PATCH', `${store.state.baseURL}/api/resource${from}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.setRequestHeader('Destination', to)
if (copy) {
@@ -158,7 +162,7 @@ export function checksum (url, algo) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/checksum${url}?algo=${algo}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
@@ -222,7 +226,7 @@ export function getSettings () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/settings/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -251,7 +255,7 @@ export function updateSettings (param, which) {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -274,7 +278,7 @@ export function getUsers () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -295,7 +299,7 @@ export function getUser (id) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -316,7 +320,7 @@ export function newUser (user) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/users/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -341,7 +345,7 @@ export function updateUser (user, which) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -366,7 +370,7 @@ export function deleteUser (id) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -391,7 +395,7 @@ export function getShare (url) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
@@ -410,7 +414,7 @@ export function deleteShare (hash) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
@@ -435,7 +439,7 @@ export function share (url, expires = '', unit = 'hours') {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', url, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {

View File

@@ -1,13 +1,18 @@
import cookie from './cookie'
import store from '@/store'
import router from '@/router'
import { Base64 } from 'js-base64'
function parseToken (token) {
let path = store.state.baseURL
if (path === '') path = '/'
document.cookie = `auth=${token}; max-age=86400; path=${path}`
let res = token.split('.')
let user = JSON.parse(window.atob(res[1]))
let user = JSON.parse(Base64.decode(res[1]))
if (!user.commands) {
user.commands = []
}
store.commit('setJWT', token)
store.commit('setUser', user)
}
@@ -16,7 +21,7 @@ function loggedIn () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/auth/renew`, true)
request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`)
request.onload = () => {
if (request.status === 200) {
@@ -31,8 +36,8 @@ function loggedIn () {
})
}
function login (user, password) {
let data = {username: user, password: password}
function login (user, password, captcha) {
let data = {username: user, password: password, recaptcha: captcha}
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/auth/get`, true)

View File

@@ -210,7 +210,7 @@ export default {
}
},
scroll (event) {
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
if (this.req.kind !== 'listing' || this.$store.state.user.viewMode === 'mosaic') return
let top = 112 - window.scrollY

View File

@@ -1,162 +0,0 @@
<template>
<div class="dashboard">
<ul id="nav">
<li>
<router-link to="/settings/profile">
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.profileSettings') }}
</router-link>
</li>
<li>
<router-link to="/users">
{{ $t('settings.userManagement') }} <i class="material-icons">keyboard_arrow_right</i>
</router-link>
</li>
</ul>
<h1>{{ $t('settings.globalSettings') }}</h1>
<form @submit="saveStaticGen" v-if="$store.state.staticGen.length > 0">
<h2>{{ capitalize($store.state.staticGen) }}</h2>
<p v-for="field in staticGen" :key="field.variable">
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
</p>
<p><input type="submit" value="Save"></p>
</form>
<form @submit="saveCommands">
<h2>{{ $t('settings.commands') }}</h2>
<p class="small">{{ $t('settings.commandsHelp') }}</p>
<template v-for="command in commands">
<h3>{{ capitalize(command.name) }}</h3>
<textarea v-model.trim="command.value"></textarea>
</template>
<p><input type="submit" value="Save"></p>
</form>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { getSettings, updateSettings } from '@/utils/api'
export default {
name: 'settings',
data: function () {
return {
commands: [],
staticGen: []
}
},
computed: {
...mapState([ 'user' ])
},
created () {
getSettings()
.then(settings => {
if (this.$store.state.staticGen.length > 0) {
this.parseStaticGen(settings.staticGen)
}
for (let key in settings.commands) {
this.commands.push({
name: key,
value: settings.commands[key].join('\n')
})
}
})
.catch(error => { this.showError(error) })
},
methods: {
...mapMutations([ 'showSuccess', 'showError' ]),
capitalize (name, where = '_') {
if (where === 'caps') where = /(?=[A-Z])/
let splitted = name.split(where)
name = ''
for (let i = 0; i < splitted.length; i++) {
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
}
return name.slice(0, -1)
},
saveCommands (event) {
event.preventDefault()
let commands = {}
for (let command of this.commands) {
let value = command.value.split('\n')
if (value.length === 1 && value[0] === '') {
value = []
}
commands[command.name] = value
}
updateSettings(commands, 'commands')
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
.catch(error => { this.showError(error) })
},
saveStaticGen (event) {
event.preventDefault()
let staticGen = {}
for (let field of this.staticGen) {
staticGen[field.variable] = field.value
if (field.original === 'array') {
let val = field.value.split(' ')
if (val[0] === '') {
val.shift()
}
staticGen[field.variable] = val
}
}
updateSettings(staticGen, 'staticGen')
.then(() => { this.showSuccess(this.$t('settings.settingsUpdated')) })
.catch(error => { this.showError(error) })
},
parseStaticGen (staticgen) {
for (let option of staticgen) {
let value = option.value
let field = {
name: option.name,
variable: option.variable,
type: 'text',
original: 'text',
value: value
}
if (Array.isArray(value)) {
field.original = 'array'
field.value = value.join(' ')
this.staticGen.push(field)
continue
}
switch (typeof value) {
case 'boolean':
field.type = 'checkbox'
field.original = 'boolean'
break
}
this.staticGen.push(field)
}
}
}
}
</script>

View File

@@ -1,9 +1,12 @@
<template>
<div>
<div id="progress">
<div v-bind:style="{ width: $store.state.progress + '%' }"></div>
</div>
<site-header></site-header>
<sidebar></sidebar>
<main>
<router-view v-on:css-updated="updateCSS"></router-view>
<router-view @css="$emit('update:css')"></router-view>
</main>
<prompts></prompts>
</div>
@@ -31,23 +34,10 @@ export default {
}
},
mounted () {
this.updateCSS()
this.$emit('update:css')
},
methods: {
updateCSS () {
let css = this.$store.state.user.css
let style = document.querySelector('style[title="user-css"]')
if (style !== undefined && style !== null) {
style.parentElement.removeChild(style)
}
style = document.createElement('style')
style.title = 'user-css'
style.type = 'text/css'
style.appendChild(document.createTextNode(css))
document.head.appendChild(style)
}
beforeDestroy () {
this.$emit('clean:css')
}
}
</script>

View File

@@ -1,11 +1,12 @@
<template>
<div id="login">
<div id="login" :class="{ recaptcha: recaptcha.length > 0 }">
<form @submit="submit">
<img src="../assets/logo.svg" alt="File Manager">
<h1>File Manager</h1>
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
<input type="text" v-model="username" :placeholder="$t('login.username')">
<input type="password" v-model="password" :placeholder="$t('login.password')">
<div v-if="recaptcha.length" id="recaptcha"></div>
<input type="submit" :value="$t('login.submit')">
</form>
</div>
@@ -13,9 +14,12 @@
<script>
import auth from '@/utils/auth'
import { mapState } from 'vuex'
export default {
name: 'login',
props: ['dependencies'],
computed: mapState(['recaptcha']),
data: function () {
return {
wrong: false,
@@ -23,8 +27,23 @@ export default {
password: ''
}
},
mounted () {
if (this.dependencies) this.setup()
},
watch: {
dependencies: function (val) {
if (val) this.setup()
}
},
methods: {
submit: function (event) {
setup () {
if (this.recaptcha.length === 0) return
window.grecaptcha.render('recaptcha', {
sitekey: this.recaptcha
})
},
submit (event) {
event.preventDefault()
event.stopPropagation()
@@ -33,7 +52,17 @@ export default {
redirect = '/files/'
}
auth.login(this.username, this.password)
let captcha = ''
if (this.recaptcha.length > 0) {
captcha = window.grecaptcha.getResponse()
if (captcha === '') {
this.wrong = true
return
}
}
auth.login(this.username, this.password, captcha)
.then(() => { this.$router.push({ path: redirect }) })
.catch(() => { this.wrong = true })
}

View File

@@ -1,103 +0,0 @@
<template>
<div class="dashboard">
<ul id="nav" v-if="user.admin">
<li>
<router-link to="/settings/global">
{{ $t('settings.globalSettings') }} <i class="material-icons">keyboard_arrow_right</i>
</router-link>
</li>
</ul>
<h1>{{ $t('settings.profileSettings') }}</h1>
<form @submit="updateSettings">
<h3>{{ $t('settings.language') }}</h3>
<p><languages id="locale" :selected.sync="locale"></languages></p>
<h3>{{ $t('settings.customStylesheet') }}</h3>
<textarea v-model="css" name="css"></textarea>
<p><input type="submit" :value="$t('buttons.update')"></p>
</form>
<form @submit="updatePassword">
<h3>{{ $t('settings.changePassword') }}</h3>
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p>
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p>
<p><input type="submit" :value="$t('buttons.update')"></p>
</form>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { updateUser } from '@/utils/api'
import Languages from '@/components/Languages'
export default {
name: 'settings',
components: {
Languages
},
data: function () {
return {
password: '',
passwordConf: '',
css: '',
locale: ''
}
},
computed: {
...mapState([ 'user' ]),
passwordClass () {
if (this.password === '' && this.passwordConf === '') {
return ''
}
if (this.password === this.passwordConf) {
return 'green'
}
return 'red'
}
},
created () {
this.css = this.user.css
this.locale = this.user.locale
},
methods: {
...mapMutations([ 'showSuccess' ]),
updatePassword (event) {
event.preventDefault()
if (this.password !== this.passwordConf) {
return
}
let user = {
ID: this.$store.state.user.ID,
password: this.password
}
updateUser(user, 'password').then(location => {
this.showSuccess(this.$t('settings.passwordUpdated'))
}).catch(e => {
this.$store.commit('showError', e)
})
},
updateSettings (event) {
event.preventDefault()
let user = {...this.$store.state.user}
user.css = this.css
user.locale = this.locale
updateUser(user, 'partial').then(location => {
this.$store.commit('setUser', user)
this.$emit('css-updated')
this.showSuccess(this.$t('settings.settingsUpdated'))
}).catch(e => {
this.$store.commit('showError', e)
})
}
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div class="dashboard">
<ul id="nav" v-if="user.admin">
<li :class="{ active: $route.path === '/settings/profile' }"><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
<li :class="{ active: $route.path === '/settings/global' }"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
<li :class="{ active: $route.path === '/settings/users' }"><router-link to="/settings/users">{{ $t('settings.userManagement') }}</router-link></li>
</ul>
<router-view @css="$emit('css')"></router-view>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'settings',
computed: mapState([ 'user' ])
}
</script>

View File

@@ -1,296 +0,0 @@
<template>
<div>
<form @submit="save" class="dashboard">
<ul id="nav">
<li>
<router-link to="/users">
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.userManagement') }}
</router-link>
</li>
<li></li>
</ul>
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
<p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></p>
<p><label for="password">{{ $t('settings.password') }}</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
<p><label for="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p>
<p>
<label for="locale">{{ $t('settings.language') }}</label>
<languages id="locale" :selected.sync="locale"></languages>
</p>
<h2>{{ $t('settings.permissions') }}</h2>
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
<p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
<h3>{{ $t('settings.userCommands') }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
<input type="text" v-model.trim="commands">
<h2>{{ $t('settings.rules') }}</h2>
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
<i18n path="settings.rulesHelp2" tag="p" class="small">
<code>allow</code><code>disallow</code><code>regex</code>
</i18n>
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
<ul class="small">
<li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
</ul>
<textarea v-model.trim="rules"></textarea>
<h2>{{ $t('settings.customStylesheet') }}</h2>
<textarea name="css"></textarea>
<p>
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
<input type="submit" :value="$t('buttons.save')">
</p>
</form>
<div v-if="$store.state.show === 'deleteUser'" class="prompt">
<h3>Delete User</h3>
<p>Are you sure you want to delete this user?</p>
<div>
<button @click="deleteUser" autofocus>{{ $t('buttons.delete') }}</button>
<button class="cancel"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">
{{ $t('buttons.cancel') }}
</button>
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
import Languages from '@/components/Languages'
export default {
name: 'user',
components: { Languages },
data: () => {
return {
id: 0,
admin: false,
allowNew: false,
allowEdit: false,
allowCommands: false,
allowPublish: false,
permissions: {},
password: '',
username: '',
filesystem: '',
rules: '',
locale: '',
css: '',
commands: ''
}
},
computed: {
passwordPlaceholder () {
if (this.$route.path === '/users/new') return ''
return this.$t('settings.avoidChanges')
}
},
created () {
this.fetchData()
},
watch: {
'$route': 'fetchData',
admin: function () {
if (!this.admin) return
this.allowCommands = true
this.allowEdit = true
this.allowNew = true
this.allowPublish = true
for (let key in this.permissions) {
this.permissions[key] = true
}
}
},
methods: {
...mapMutations(['closeHovers']),
fetchData () {
let user = this.$route.params[0]
if (this.$route.path === '/users/new') {
user = 'base'
}
getUser(user).then(user => {
this.id = user.ID
this.admin = user.admin
this.allowCommands = user.allowCommands
this.allowNew = user.allowNew
this.allowEdit = user.allowEdit
this.allowPublish = user.allowPublish
this.filesystem = user.filesystem
this.username = user.username
this.commands = user.commands.join(' ')
this.css = user.css
this.permissions = user.permissions
this.locale = user.locale
for (let rule of user.rules) {
if (rule.allow) {
this.rules += 'allow '
} else {
this.rules += 'disallow '
}
if (rule.regex) {
this.rules += 'regex ' + rule.regexp.raw
} else {
this.rules += rule.path
}
this.rules += '\n'
}
this.rules = this.rules.trim()
}).catch(() => {
this.$router.push({ path: '/users/new' })
})
},
capitalize (name) {
let splitted = name.split(/(?=[A-Z])/)
name = ''
for (let i = 0; i < splitted.length; i++) {
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
}
return name.slice(0, -1)
},
reset () {
this.id = 0
this.admin = false
this.allowNew = false
this.allowEdit = false
this.allowPublish = false
this.permissins = {}
this.allowCommands = false
this.password = ''
this.username = ''
this.filesystem = ''
this.rules = ''
this.locale = ''
this.css = ''
this.commands = ''
},
deletePrompt (event) {
this.$store.commit('showHover', 'deleteUser')
},
deleteUser (event) {
event.preventDefault()
deleteUser(this.id).then(location => {
this.$router.push({ path: '/users' })
this.$store.commit('showSuccess', this.$t('settings.userDeleted'))
}).catch(e => {
this.$store.commit('showError', e)
})
},
save (event) {
event.preventDefault()
let user = this.parseForm()
if (this.$route.path === '/users/new') {
newUser(user).then(location => {
this.$router.push({ path: location })
this.$store.commit('showSuccess', this.$t('settings.userCreated'))
}).catch(e => {
this.$store.commit('showError', e)
})
return
}
updateUser(user).then(location => {
if (user.ID === this.$store.state.user.ID) {
this.$store.commit('setUser', user)
}
this.$store.commit('showSuccess', this.$t('settings.userUpdated'))
}).catch(e => {
this.$store.commit('showError', e)
})
},
parseForm () {
let user = {
ID: this.id,
username: this.username,
password: this.password,
filesystem: this.filesystem,
admin: this.admin,
allowCommands: this.allowCommands,
allowNew: this.allowNew,
allowEdit: this.allowEdit,
allowPublish: this.allowPublish,
permissions: this.permissions,
css: this.css,
locale: this.locale,
commands: this.commands.split(' '),
rules: []
}
let rules = this.rules.split('\n')
for (let rawRule of rules) {
let rule = {
allow: true,
path: '',
regex: false,
regexp: {
raw: ''
}
}
rawRule = rawRule.split(' ')
// Skip a malformed rule
if (rawRule.length < 2) {
continue
}
// Skip a malformed rule
if (rawRule[0] !== 'allow' && rawRule[0] !== 'disallow') {
continue
}
rule.allow = (rawRule[0] === 'allow')
rawRule.shift()
if (rawRule[0] === 'regex') {
rule.regex = true
rawRule.shift()
rule.regexp.raw = rawRule.join(' ')
} else {
rule.path = rawRule.join(' ')
}
user.rules.push(rule)
}
return user
}
}
}
</script>

View File

@@ -1,51 +0,0 @@
<template>
<div class="dashboard">
<ul id="nav">
<li>
<router-link to="/settings/global">
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.globalSettings') }}
</router-link>
</li>
<li></li>
</ul>
<h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1>
<table>
<tr>
<th>{{ $t('settings.username') }}</th>
<th>{{ $t('settings.admin') }}</th>
<th>{{ $t('settings.scope') }}</th>
<th></th>
</tr>
<tr v-for="user in users">
<td>{{ user.username }}</td>
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
<td>{{ user.filesystem }}</td>
<td><router-link :to="'/users/' + user.ID"><i class="material-icons">mode_edit</i></router-link></td>
</tr>
</table>
</div>
</template>
<script>
import * as api from '@/utils/api'
export default {
name: 'users',
data: function () {
return {
users: []
}
},
created () {
api.getUsers().then(users => {
this.users = users
}).catch(error => {
this.$store.commit('showError', error)
})
}
}
</script>

View File

@@ -0,0 +1,187 @@
<template>
<div class="dashboard">
<form class="card" v-if="staticGen.length" @submit.prevent="saveStaticGen">
<div class="card-title">
<h2>{{ capitalize($store.state.staticGen) }}</h2>
</div>
<div class="card-content">
<p v-for="field in staticGen" :key="field.variable">
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
</p>
</div>
<div class="card-action">
<input class="flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
<form class="card" @submit.prevent="saveCSS">
<div class="card-title">
<h2>{{ $t('settings.customStylesheet') }}</h2>
</div>
<div class="card-content">
<textarea v-model="css"></textarea>
</div>
<div class="card-action">
<input class="flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
<form class="card" @submit.prevent="saveCommands">
<div class="card-title">
<h2>{{ $t('settings.commands') }}</h2>
</div>
<div class="card-content">
<p class="small">{{ $t('settings.commandsHelp') }}</p>
<div v-for="command in commands" :key="command.name" class="collapsible">
<input :id="command.name" type="checkbox">
<label :for="command.name">
<p>{{ capitalize(command.name) }}</p>
<i class="material-icons">arrow_drop_down</i>
</label>
<div class="collapse">
<textarea v-model.trim="command.value"></textarea>
</div>
</div>
</div>
<div class="card-action">
<input class="flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { getSettings, updateSettings } from '@/utils/api'
export default {
name: 'settings',
data: function () {
return {
commands: [],
staticGen: [],
css: ''
}
},
computed: {
...mapState([ 'user' ])
},
created () {
getSettings()
.then(settings => {
if (this.$store.state.staticGen.length > 0) {
this.parseStaticGen(settings.staticGen)
}
for (let key in settings.commands) {
this.commands.push({
name: key,
value: settings.commands[key].join('\n')
})
}
this.css = settings.css
})
.catch(this.$showError)
},
methods: {
capitalize (name, where = '_') {
if (where === 'caps') where = /(?=[A-Z])/
let splitted = name.split(where)
name = ''
for (let i = 0; i < splitted.length; i++) {
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
}
return name.slice(0, -1)
},
saveCommands (event) {
let commands = {}
for (let command of this.commands) {
let value = command.value.split('\n')
if (value.length === 1 && value[0] === '') {
value = []
}
commands[command.name] = value
}
updateSettings(commands, 'commands')
.then(() => { this.$showSuccess(this.$t('settings.commandsUpdated')) })
.catch(this.$showError)
},
saveCSS (event) {
updateSettings(this.css, 'css')
.then(() => {
this.$showSuccess(this.$t('settings.settingsUpdated'))
this.$store.commit('setCSS', this.css)
this.$emit('css')
})
.catch(this.$showError)
},
saveStaticGen (event) {
let staticGen = {}
for (let field of this.staticGen) {
staticGen[field.variable] = field.value
if (field.original === 'array') {
let val = field.value.split(' ')
if (val[0] === '') {
val.shift()
}
staticGen[field.variable] = val
}
}
updateSettings(staticGen, 'staticGen')
.then(() => { this.$showSuccess(this.$t('settings.settingsUpdated')) })
.catch(this.$showError)
},
parseStaticGen (staticgen) {
for (let option of staticgen) {
let value = option.value
let field = {
name: option.name,
variable: option.variable,
type: 'text',
original: 'text',
value: value
}
if (Array.isArray(value)) {
field.original = 'array'
field.value = value.join(' ')
this.staticGen.push(field)
continue
}
switch (typeof value) {
case 'boolean':
field.type = 'checkbox'
field.original = 'boolean'
break
}
this.staticGen.push(field)
}
}
}
}
</script>

View File

@@ -0,0 +1,109 @@
<template>
<div class="dashboard">
<form class="card" @submit="updateSettings">
<div class="card-title">
<h2>{{ $t('settings.profileSettings') }}</h2>
</div>
<div class="card-content">
<h3>{{ $t('settings.language') }}</h3>
<p><languages id="locale" :selected.sync="locale"></languages></p>
<h3>{{ $t('settings.customStylesheet') }}</h3>
<textarea v-model="css" name="css"></textarea>
</div>
<div class="card-action">
<input class="flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
<form class="card" v-if="!user.lockPassword" @submit="updatePassword">
<div class="card-title">
<h2>{{ $t('settings.changePassword') }}</h2>
</div>
<div class="card-content">
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p>
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p>
</div>
<div class="card-action">
<input class="flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { updateUser } from '@/utils/api'
import Languages from '@/components/Languages'
export default {
name: 'settings',
components: {
Languages
},
data: function () {
return {
password: '',
passwordConf: '',
css: '',
locale: ''
}
},
computed: {
...mapState([ 'user' ]),
passwordClass () {
if (this.password === '' && this.passwordConf === '') {
return ''
}
if (this.password === this.passwordConf) {
return 'green'
}
return 'red'
}
},
created () {
this.css = this.user.css
this.locale = this.user.locale
},
methods: {
updatePassword (event) {
event.preventDefault()
if (this.password !== this.passwordConf) {
return
}
let user = {
ID: this.$store.state.user.ID,
password: this.password
}
updateUser(user, 'password').then(location => {
this.$showSuccess(this.$t('settings.passwordUpdated'))
}).catch(e => {
this.$showError(e)
})
},
updateSettings (event) {
event.preventDefault()
let user = {...this.$store.state.user}
user.css = this.css
user.locale = this.locale
updateUser(user, 'partial').then(location => {
this.$store.commit('setUser', user)
this.$emit('css')
this.$showSuccess(this.$t('settings.settingsUpdated'))
}).catch(e => {
this.$showError(e)
})
}
}
}
</script>

View File

@@ -0,0 +1,319 @@
<template>
<div>
<form @submit="save" class="card">
<div class="card-title">
<h2 v-if="id === 0">{{ $t('settings.newUser') }}</h2>
<h2 v-else>{{ $t('settings.user') }} {{ username }}</h2>
</div>
<div class="card-content">
<p>
<label for="username">{{ $t('settings.username') }}</label>
<input type="text" v-model="username" id="username">
</p>
<p>
<label for="password">{{ $t('settings.password') }}</label>
<input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password">
</p>
<p>
<label for="scope">{{ $t('settings.scope') }}</label>
<input type="text" v-model="filesystem" id="scope">
</p>
<p>
<label for="locale">{{ $t('settings.language') }}</label>
<languages id="locale" :selected.sync="locale"></languages>
</p>
<p><input type="checkbox" :disabled="admin" v-model="lockPassword"> {{ $t('settings.lockPassword') }}</p>
<h3>{{ $t('settings.permissions') }}</h3>
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
<p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
<h3>{{ $t('settings.userCommands') }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
<input type="text" v-model.trim="commands">
<h3>{{ $t('settings.rules') }}</h3>
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
<i18n path="settings.rulesHelp2" tag="p" class="small">
<code>allow</code><code>disallow</code><code>regex</code>
</i18n>
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
<ul class="small">
<li><code>disallow regex [\\\/]\..+</code> - {{ $t('settings.ruleExample1') }}</li>
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
</ul>
<textarea v-model.trim="rules"></textarea>
<h3>{{ $t('settings.customStylesheet') }}</h3>
<textarea name="css"></textarea>
</div>
<div class="card-action">
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="flat delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
<input class="flat" type="submit" :value="$t('buttons.save')">
</div>
</form>
<div v-if="$store.state.show === 'deleteUser'" class="card floating">
<div class="card-content">
<p>Are you sure you want to delete this user?</p>
</div>
<div class="card-action">
<button class="cancel flat"
@click="closeHovers"
autofocus
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">
{{ $t('buttons.cancel') }}
</button>
<button class="flat"
@click="deleteUser">
{{ $t('buttons.delete') }}
</button>
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
import Languages from '@/components/Languages'
export default {
name: 'user',
components: { Languages },
data: () => {
return {
originalUser: null,
id: 0,
admin: false,
allowNew: false,
allowEdit: false,
allowCommands: false,
allowPublish: false,
lockPassword: false,
permissions: {},
password: '',
username: '',
filesystem: '',
rules: '',
locale: '',
css: '',
commands: ''
}
},
computed: {
passwordPlaceholder () {
if (this.$route.path === '/settings/users/new') return ''
return this.$t('settings.avoidChanges')
}
},
created () {
this.fetchData()
},
watch: {
'$route': 'fetchData',
admin: function () {
if (!this.admin) return
this.allowCommands = true
this.allowEdit = true
this.allowNew = true
this.allowPublish = true
this.lockPassword = false
for (let key in this.permissions) {
this.permissions[key] = true
}
}
},
methods: {
...mapMutations(['closeHovers']),
fetchData () {
let user = this.$route.params[0]
if (this.$route.path === '/settings/users/new') {
user = 'base'
}
getUser(user).then(user => {
this.originalUser = user
this.id = user.ID
this.admin = user.admin
this.allowCommands = user.allowCommands
this.allowNew = user.allowNew
this.allowEdit = user.allowEdit
this.allowPublish = user.allowPublish
this.lockPassword = user.lockPassword
this.filesystem = user.filesystem
this.username = user.username
this.css = user.css
this.permissions = user.permissions
this.locale = user.locale
if (user.commands) {
this.commands = user.commands.join(' ')
}
for (let rule of user.rules) {
if (rule.allow) {
this.rules += 'allow '
} else {
this.rules += 'disallow '
}
if (rule.regex) {
this.rules += 'regex ' + rule.regexp.raw
} else {
this.rules += rule.path
}
this.rules += '\n'
}
this.rules = this.rules.trim()
}).catch(() => {
this.$router.push({ path: '/settings/users/new' })
})
},
capitalize (name) {
let splitted = name.split(/(?=[A-Z])/)
name = ''
for (let i = 0; i < splitted.length; i++) {
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
}
return name.slice(0, -1)
},
reset () {
this.id = 0
this.admin = false
this.allowNew = false
this.allowEdit = false
this.allowPublish = false
this.permissins = {}
this.allowCommands = false
this.lockPassword = false
this.password = ''
this.username = ''
this.filesystem = ''
this.rules = ''
this.locale = ''
this.css = ''
this.commands = ''
},
deletePrompt (event) {
this.$store.commit('showHover', 'deleteUser')
},
deleteUser (event) {
event.preventDefault()
deleteUser(this.id).then(location => {
this.$router.push({ path: '/settings/users' })
this.$showSuccess(this.$t('settings.userDeleted'))
}).catch(e => {
this.$showError(e)
})
},
save (event) {
event.preventDefault()
let user = this.parseForm()
if (this.$route.path === '/settings/users/new') {
newUser(user).then(location => {
this.$router.push({ path: location })
this.$showSuccess(this.$t('settings.userCreated'))
}).catch(e => {
this.$showError(e)
})
return
}
updateUser(user).then(location => {
if (user.ID === this.$store.state.user.ID) {
this.$store.commit('setUser', user)
}
this.$showSuccess(this.$t('settings.userUpdated'))
}).catch(e => {
this.$showError(e)
})
},
parseForm () {
let user = this.originalUser
user.username = this.username
user.password = this.password
user.lockPassword = this.lockPassword
user.filesystem = this.filesystem
user.admin = this.admin
user.allowCommands = this.allowCommands
user.allowNew = this.allowNew
user.allowEdit = this.allowEdit
user.allowPublish = this.allowPublish
user.permissions = this.permissions
user.css = this.css
user.locale = this.locale
user.commands = this.commands.split(' ')
user.rules = []
let rules = this.rules.split('\n')
for (let rawRule of rules) {
let rule = {
allow: true,
path: '',
regex: false,
regexp: {
raw: ''
}
}
rawRule = rawRule.split(' ')
// Skip a malformed rule
if (rawRule.length < 2) {
continue
}
// Skip a malformed rule
if (rawRule[0] !== 'allow' && rawRule[0] !== 'disallow') {
continue
}
rule.allow = (rawRule[0] === 'allow')
rawRule.shift()
if (rawRule[0] === 'regex') {
rule.regex = true
rawRule.shift()
rule.regexp.raw = rawRule.join(' ')
} else {
rule.path = rawRule.join(' ')
}
user.rules.push(rule)
}
return user
}
}
}
</script>

View File

@@ -0,0 +1,48 @@
<template>
<div class="card">
<div class="card-title">
<h2>{{ $t('settings.users') }}</h2>
<router-link to="/settings/users/new"><button class="flat">{{ $t('buttons.new') }}</button></router-link>
</div>
<div class="card-content full">
<table>
<tr>
<th>{{ $t('settings.username') }}</th>
<th>{{ $t('settings.admin') }}</th>
<th>{{ $t('settings.scope') }}</th>
<th></th>
</tr>
<tr v-for="user in users" :key="user.id">
<td>{{ user.username }}</td>
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
<td>{{ user.filesystem }}</td>
<td class="small">
<router-link :to="'/settings/users/' + user.ID"><i class="material-icons">mode_edit</i></router-link>
</td>
</tr>
</table>
</div>
</div>
</template>
<script>
import * as api from '@/utils/api'
export default {
name: 'users',
data: function () {
return {
users: []
}
},
created () {
api.getUsers().then(users => {
this.users = users
}).catch(error => {
this.$showError(error)
})
}
}
</script>

View File

@@ -1,92 +0,0 @@
package filemanager
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
var defaultCredentials = "{\"username\":\"admin\",\"password\":\"admin\"}"
var authHandlerTests = []struct {
Data string
Expected int
}{
{defaultCredentials, http.StatusOK},
{"{\"username\":\"admin\",\"password\":\"wrong\"}", http.StatusForbidden},
{"{\"username\":\"wrong\",\"password\":\"admin\"}", http.StatusForbidden},
}
func TestAuthHandler(t *testing.T) {
fm := newTest(t)
defer fm.Clean()
for _, test := range authHandlerTests {
req, err := http.NewRequest("POST", "/api/auth/get", strings.NewReader(test.Data))
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
fm.ServeHTTP(w, req)
if w.Code != test.Expected {
t.Errorf("Wrong status code: got %v want %v", w.Code, test.Expected)
}
}
}
func TestRenewHandler(t *testing.T) {
fm := newTest(t)
defer fm.Clean()
// First, we have to make an auth request to get the user authenticated,
r, err := http.NewRequest("POST", "/api/auth/get", strings.NewReader(defaultCredentials))
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
fm.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Couldn't authenticate: got %v", w.Code)
}
token := w.Body.String()
// Test renew authorization via Authorization Header.
r, err = http.NewRequest("GET", "/api/auth/renew", nil)
if err != nil {
t.Fatal(err)
}
r.Header.Set("Authorization", "Bearer "+token)
w = httptest.NewRecorder()
fm.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Can't renew auth via header: got %v", w.Code)
}
// Test renew authorization via cookie field.
r, err = http.NewRequest("GET", "/api/auth/renew", nil)
if err != nil {
t.Fatal(err)
}
r.AddCookie(&http.Cookie{
Value: token,
Name: "auth",
Expires: time.Now().Add(1 * time.Hour),
})
w = httptest.NewRecorder()
fm.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Can't renew auth via cookie: got %v", w.Code)
}
}

26
bolt/config.go Normal file
View File

@@ -0,0 +1,26 @@
package bolt
import (
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
// 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 fm.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)
}

66
bolt/share.go Normal file
View File

@@ -0,0 +1,66 @@
package bolt
import (
"github.com/asdine/storm"
"github.com/asdine/storm/q"
fm "github.com/hacdias/filemanager"
)
// 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) (*fm.ShareLink, error) {
var v fm.ShareLink
err := s.DB.One("Hash", hash, &v)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
return &v, err
}
// GetPermanent gets the permanent link from a path.
func (s ShareStore) GetPermanent(path string) (*fm.ShareLink, error) {
var v fm.ShareLink
err := s.DB.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&v)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
return &v, err
}
// GetByPath gets all the links for a specific path.
func (s ShareStore) GetByPath(hash string) ([]*fm.ShareLink, error) {
var v []*fm.ShareLink
err := s.DB.Find("Path", hash, &v)
if err == storm.ErrNotFound {
return v, fm.ErrNotExist
}
return v, err
}
// Gets retrieves all the shareable links.
func (s ShareStore) Gets() ([]*fm.ShareLink, error) {
var v []*fm.ShareLink
err := s.DB.All(&v)
if err == storm.ErrNotFound {
return v, fm.ErrNotExist
}
return v, err
}
// Save stores a Share Link on the database.
func (s ShareStore) Save(l *fm.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(&fm.ShareLink{Hash: hash})
}

90
bolt/users.go Normal file
View File

@@ -0,0 +1,90 @@
package bolt
import (
"reflect"
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
// 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 fm.FSBuilder) (*fm.User, error) {
var us fm.User
err := u.DB.One("ID", id, &us)
if err == storm.ErrNotFound {
return nil, fm.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 fm.FSBuilder) (*fm.User, error) {
var us fm.User
err := u.DB.One("Username", username, &us)
if err == storm.ErrNotFound {
return nil, fm.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 fm.FSBuilder) ([]*fm.User, error) {
var us []*fm.User
err := u.DB.All(&us)
if err == storm.ErrNotFound {
return nil, fm.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 *fm.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 *fm.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(&fm.User{ID: id})
}

View File

@@ -1,4 +1,5 @@
#!/bin/bash
set -e
# Install rice tool if not present
if ! [ -x "$(command -v rice)" ]; then

View File

@@ -4,17 +4,11 @@
package filemanager
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
. "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
@@ -28,23 +22,18 @@ func init() {
type plugin struct {
Next httpserver.Handler
Configs []*config
}
type config struct {
*FileManager
baseURL string
Configs []*filemanager.FileManager
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs {
// Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].baseURL) {
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue
}
f.Configs[i].ServeHTTP(w, r)
h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil
}
@@ -53,7 +42,7 @@ func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error {
configs, err := parse(c)
configs, err := parser.Parse(c, "")
if err != nil {
return err
}
@@ -64,107 +53,3 @@ func setup(c *caddy.Controller) error {
return nil
}
func parse(c *caddy.Controller) ([]*config, error) {
var (
configs []*config
)
for c.Next() {
baseURL := "/"
baseScope := "."
database := ""
noAuth := false
// Get the baseURL and baseScope
args := c.RemainingArgs()
if len(args) >= 1 {
baseURL = args[0]
}
if len(args) > 1 {
baseScope = args[1]
}
for c.NextBlock() {
switch c.Val() {
case "database":
if !c.NextArg() {
return nil, c.ArgErr()
}
database = c.Val()
case "no_auth":
if !c.NextArg() {
noAuth = true
continue
}
var err error
noAuth, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
}
}
caddyConf := httpserver.GetConfig(c)
path := filepath.Join(caddy.AssetsPath(), "filemanager")
err := os.MkdirAll(path, 0700)
if err != nil {
return nil, err
}
// if there is a database path and it is not absolute,
// it will be relative to Caddy folder.
if !filepath.IsAbs(database) && database != "" {
database = filepath.Join(path, database)
}
// If there is no database path on the settings,
// store one in .caddy/filemanager/name.db.
if database == "" {
// The name of the database is the hashed value of a string composed
// by the host, address path and the baseurl of this File Manager
// instance.
hasher := md5.New()
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL))
sha := hex.EncodeToString(hasher.Sum(nil))
database = filepath.Join(path, sha+".db")
fmt.Println("[WARNING] A database is going to be created for your File Manager instace at " + database +
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
}
fm, err := New(database, User{
Locale: "en",
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
Commands: []string{"git", "svn", "hg"},
Rules: []*Rule{{
Regex: true,
Allow: false,
Regexp: &Regexp{Raw: "\\/\\..+"},
}},
CSS: "",
FileSystem: fileutils.Dir(baseScope),
})
if err != nil {
return nil, err
}
fm.NoAuth = noAuth
m := &config{FileManager: fm}
m.SetBaseURL(baseURL)
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
m.baseURL = strings.TrimSuffix(baseURL, "/")
configs = append(configs, m)
}
return configs, nil
}

View File

@@ -1,169 +1,15 @@
package hugo
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
"github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error {
configs, err := parse(c)
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next}
})
return nil
}
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
var (
configs []*filemanager.FileManager
)
for c.Next() {
// hugo [directory] [admin] {
// database path
// }
directory := "."
admin := "/admin"
database := ""
noAuth := false
// Get the baseURL and baseScope
args := c.RemainingArgs()
if len(args) >= 1 {
directory = args[0]
}
if len(args) > 1 {
admin = args[1]
}
for c.NextBlock() {
switch c.Val() {
case "database":
if !c.NextArg() {
return nil, c.ArgErr()
}
database = c.Val()
case "no_auth":
if !c.NextArg() {
noAuth = true
continue
}
var err error
noAuth, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
}
}
caddyConf := httpserver.GetConfig(c)
path := filepath.Join(caddy.AssetsPath(), "hugo")
err := os.MkdirAll(path, 0700)
if err != nil {
return nil, err
}
// if there is a database path and it is not absolute,
// it will be relative to ".caddy" folder.
if !filepath.IsAbs(database) && database != "" {
database = filepath.Join(path, database)
}
// If there is no database path on the settings,
// store one in .caddy/hugo/{name}.db.
if database == "" {
// The name of the database is the hashed value of a string composed
// by the host, address path and the baseurl of this File Manager
// instance.
hasher := md5.New()
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + admin))
sha := hex.EncodeToString(hasher.Sum(nil))
database = filepath.Join(path, sha+".db")
fmt.Println("[WARNING] A database is going to be created for your Hugo instace at " + database +
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
}
m, err := filemanager.New(database, filemanager.User{
Locale: "en",
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{"git", "svn", "hg"},
Rules: []*filemanager.Rule{{
Regex: true,
Allow: false,
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
}},
CSS: "",
FileSystem: fileutils.Dir(directory),
})
if err != nil {
return nil, err
}
// Initialize the default settings for Hugo.
hugo := &filemanager.Hugo{
Root: directory,
Public: filepath.Join(directory, "public"),
Args: []string{},
CleanPublic: true,
}
// Attaches Hugo plugin to this file manager instance.
err = m.EnableStaticGen(hugo)
if err != nil {
return nil, err
}
m.NoAuth = noAuth
m.SetBaseURL(admin)
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
configs = append(configs, m)
}
return configs, nil
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
func (p plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range p.Configs {
// Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(p.Configs[i].BaseURL) {
continue
}
p.Configs[i].ServeHTTP(w, r)
return 0, nil
}
return p.Next.ServeHTTP(w, r)
}
func init() {
caddy.RegisterPlugin("hugo", caddy.Plugin{
ServerType: "http",
@@ -175,3 +21,32 @@ type plugin struct {
Next httpserver.Handler
Configs []*filemanager.FileManager
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs {
// Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue
}
h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil
}
return f.Next.ServeHTTP(w, r)
}
// setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "hugo")
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next}
})
return nil
}

View File

@@ -1,169 +1,15 @@
package jekyll
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
"github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error {
configs, err := parse(c)
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next}
})
return nil
}
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
var (
configs []*filemanager.FileManager
)
for c.Next() {
// jekyll [directory] [admin] {
// database path
// }
directory := "."
admin := "/admin"
database := ""
noAuth := false
// Get the baseURL and baseScope
args := c.RemainingArgs()
if len(args) >= 1 {
directory = args[0]
}
if len(args) > 1 {
admin = args[1]
}
for c.NextBlock() {
switch c.Val() {
case "database":
if !c.NextArg() {
return nil, c.ArgErr()
}
database = c.Val()
case "no_auth":
if !c.NextArg() {
noAuth = true
continue
}
var err error
noAuth, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
}
}
caddyConf := httpserver.GetConfig(c)
path := filepath.Join(caddy.AssetsPath(), "jekyll")
err := os.MkdirAll(path, 0700)
if err != nil {
return nil, err
}
// if there is a database path and it is not absolute,
// it will be relative to ".caddy" folder.
if !filepath.IsAbs(database) && database != "" {
database = filepath.Join(path, database)
}
// If there is no database path on the settings,
// store one in .caddy/jekyll/{name}.db.
if database == "" {
// The name of the database is the hashed value of a string composed
// by the host, address path and the baseurl of this File Manager
// instance.
hasher := md5.New()
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + admin))
sha := hex.EncodeToString(hasher.Sum(nil))
database = filepath.Join(path, sha+".db")
fmt.Println("[WARNING] A database is going to be created for your Jekyll instace at " + database +
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
}
m, err := filemanager.New(database, filemanager.User{
Locale: "en",
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{"git", "svn", "hg"},
Rules: []*filemanager.Rule{{
Regex: true,
Allow: false,
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
}},
CSS: "",
FileSystem: fileutils.Dir(directory),
})
if err != nil {
return nil, err
}
// Initialize the default settings for Jekyll.
jekyll := &filemanager.Jekyll{
Root: directory,
Public: filepath.Join(directory, "_site"),
Args: []string{},
CleanPublic: true,
}
// Attaches Hugo plugin to this file manager instance.
err = m.EnableStaticGen(jekyll)
if err != nil {
return nil, err
}
m.NoAuth = noAuth
m.SetBaseURL(admin)
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
configs = append(configs, m)
}
return configs, nil
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
func (p plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range p.Configs {
// Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(p.Configs[i].BaseURL) {
continue
}
p.Configs[i].ServeHTTP(w, r)
return 0, nil
}
return p.Next.ServeHTTP(w, r)
}
func init() {
caddy.RegisterPlugin("jekyll", caddy.Plugin{
ServerType: "http",
@@ -175,3 +21,32 @@ type plugin struct {
Next httpserver.Handler
Configs []*filemanager.FileManager
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs {
// Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue
}
h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil
}
return f.Next.ServeHTTP(w, r)
}
// setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "jekyll")
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next}
})
return nil
}

295
caddy/parser/parser.go Normal file
View File

@@ -0,0 +1,295 @@
package parser
import (
"crypto/md5"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/asdine/storm"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt"
"github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
var databases = map[string]*storm.DB{}
// Parse ...
func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) {
var (
configs []*filemanager.FileManager
err error
)
for c.Next() {
u := &filemanager.User{
Locale: "en",
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{"git", "svn", "hg"},
CSS: "",
ViewMode: "mosaic",
Rules: []*filemanager.Rule{{
Regex: true,
Allow: false,
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
}},
}
baseURL := "/"
scope := "."
database := ""
noAuth := false
reCaptchaKey := ""
reCaptchaSecret := ""
if plugin != "" {
baseURL = "/admin"
}
// Get the baseURL and scope
args := c.RemainingArgs()
if plugin == "" {
if len(args) >= 1 {
baseURL = args[0]
}
if len(args) > 1 {
scope = args[1]
}
} else {
if len(args) >= 1 {
scope = args[0]
}
if len(args) > 1 {
baseURL = args[1]
}
}
for c.NextBlock() {
switch c.Val() {
case "database":
if !c.NextArg() {
return nil, c.ArgErr()
}
database = c.Val()
case "locale":
if !c.NextArg() {
return nil, c.ArgErr()
}
u.Locale = c.Val()
case "allow_commands":
if !c.NextArg() {
u.AllowCommands = true
continue
}
u.AllowCommands, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
case "allow_edit":
if !c.NextArg() {
u.AllowEdit = true
continue
}
u.AllowEdit, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
case "allow_new":
if !c.NextArg() {
u.AllowNew = true
continue
}
u.AllowNew, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
case "allow_publish":
if !c.NextArg() {
u.AllowPublish = true
continue
}
u.AllowPublish, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
case "commands":
if !c.NextArg() {
return nil, c.ArgErr()
}
u.Commands = strings.Split(c.Val(), " ")
case "css":
if !c.NextArg() {
return nil, c.ArgErr()
}
file := c.Val()
css, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
u.CSS = string(css)
case "view_mode":
if !c.NextArg() {
return nil, c.ArgErr()
}
u.ViewMode = c.Val()
if u.ViewMode != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode {
return nil, c.ArgErr()
}
case "recaptcha_key":
if !c.NextArg() {
return nil, c.ArgErr()
}
reCaptchaKey = c.Val()
case "recaptcha_secret":
if !c.NextArg() {
return nil, c.ArgErr()
}
reCaptchaSecret = c.Val()
case "no_auth":
if !c.NextArg() {
noAuth = true
continue
}
noAuth, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
}
}
caddyConf := httpserver.GetConfig(c)
path := filepath.Join(caddy.AssetsPath(), "filemanager")
err := os.MkdirAll(path, 0700)
if err != nil {
return nil, err
}
// if there is a database path and it is not absolute,
// it will be relative to Caddy folder.
if !filepath.IsAbs(database) && database != "" {
database = filepath.Join(path, database)
}
// If there is no database path on the settings,
// store one in .caddy/filemanager/name.db.
if database == "" {
// The name of the database is the hashed value of a string composed
// by the host, address path and the baseurl of this File Manager
// instance.
hasher := md5.New()
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL))
sha := hex.EncodeToString(hasher.Sum(nil))
database = filepath.Join(path, sha+".db")
fmt.Println("[WARNING] A database is going to be created for your File Manager instance at " + database +
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
}
u.Scope = scope
u.FileSystem = fileutils.Dir(scope)
var db *storm.DB
if stored, ok := databases[database]; ok {
db = stored
} else {
db, err = storm.Open(database)
databases[database] = db
}
if err != nil {
return nil, err
}
m := &filemanager.FileManager{
NoAuth: noAuth,
BaseURL: "",
PrefixURL: "",
ReCaptchaKey: reCaptchaKey,
ReCaptchaSecret: reCaptchaSecret,
DefaultUser: u,
Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope)
},
}
err = m.Setup()
if err != nil {
return nil, err
}
switch plugin {
case "hugo":
// Initialize the default settings for Hugo.
hugo := &staticgen.Hugo{
Root: scope,
Public: filepath.Join(scope, "public"),
Args: []string{},
CleanPublic: true,
}
// Attaches Hugo plugin to this file manager instance.
err = m.Attach(hugo)
if err != nil {
return nil, err
}
case "jekyll":
// Initialize the default settings for Jekyll.
jekyll := &staticgen.Jekyll{
Root: scope,
Public: filepath.Join(scope, "_site"),
Args: []string{},
CleanPublic: true,
}
// Attaches Hugo plugin to this file manager instance.
err = m.Attach(jekyll)
if err != nil {
return nil, err
}
}
if err != nil {
return nil, err
}
m.NoAuth = noAuth
m.SetBaseURL(baseURL)
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
configs = append(configs, m)
}
return configs, nil
}

View File

@@ -10,9 +10,14 @@ import (
"path/filepath"
"strings"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/asdine/storm"
"gopkg.in/natefinch/lumberjack.v2"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt"
h "github.com/hacdias/filemanager/http"
"github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
@@ -25,8 +30,13 @@ var (
scope string
commands string
logfile string
staticgen string
staticg string
locale string
baseurl string
prefixurl string
viewMode string
recaptchakey string
recaptchasecret string
port int
noAuth bool
allowCommands bool
@@ -34,7 +44,6 @@ var (
allowNew bool
allowPublish bool
showVer bool
version = "master"
)
func init() {
@@ -44,14 +53,19 @@ func init() {
flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file")
flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file")
flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users")
flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL")
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL")
flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users")
flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key")
flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret")
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users")
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users")
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users")
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
flag.StringVar(&locale, "locale", "en", "Default locale for new users")
flag.StringVar(&staticgen, "staticgen", "", "Static Generator you want to enable")
flag.StringVar(&locale, "locale", "", "Default locale for new users, set it empty to enable auto detect from browser")
flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
}
@@ -67,8 +81,13 @@ func setupViper() {
viper.SetDefault("AllowNew", true)
viper.SetDefault("AllowPublish", true)
viper.SetDefault("StaticGen", "")
viper.SetDefault("Locale", "en")
viper.SetDefault("Locale", "")
viper.SetDefault("NoAuth", false)
viper.SetDefault("BaseURL", "")
viper.SetDefault("PrefixURL", "")
viper.SetDefault("ViewMode", filemanager.MosaicViewMode)
viper.SetDefault("ReCaptchaKey", "")
viper.SetDefault("ReCaptchaSecret", "")
viper.BindPFlag("Port", flag.Lookup("port"))
viper.BindPFlag("Address", flag.Lookup("address"))
@@ -83,21 +102,18 @@ func setupViper() {
viper.BindPFlag("Locale", flag.Lookup("locale"))
viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key"))
viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret"))
viper.SetConfigName("filemanager")
viper.AddConfigPath(".")
}
func printVersion() {
version = strings.TrimSpace(version)
if version == "" {
fmt.Println("filemanager is at an untracked version")
} else {
version = strings.TrimPrefix(version, "v")
fmt.Println("filemanager version", version)
}
fmt.Println("filemanager version", filemanager.Version)
os.Exit(0)
}
@@ -148,52 +164,6 @@ func main() {
})
}
// Create a File Manager instance.
fm, err := filemanager.New(viper.GetString("Database"), filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
FileSystem: fileutils.Dir(viper.GetString("Scope")),
})
if viper.GetBool("NoAuth") {
fm.NoAuth = true
}
if err != nil {
log.Fatal(err)
}
switch viper.GetString("StaticGen") {
case "hugo":
hugo := &filemanager.Hugo{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{},
CleanPublic: true,
}
if err = fm.EnableStaticGen(hugo); err != nil {
log.Fatal(err)
}
case "jekyll":
jekyll := &filemanager.Jekyll{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"},
CleanPublic: true,
}
if err = fm.EnableStaticGen(jekyll); err != nil {
log.Fatal(err)
}
}
// Builds the address and a listener.
laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
listener, err := net.Listen("tcp", laddr)
@@ -205,7 +175,75 @@ func main() {
fmt.Println("Listening on", listener.Addr().String())
// Starts the server.
if err := http.Serve(listener, fm); err != nil {
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)
}
fm := &filemanager.FileManager{
NoAuth: viper.GetBool("NoAuth"),
BaseURL: viper.GetString("BaseURL"),
PrefixURL: viper.GetString("PrefixURL"),
ReCaptchaKey: viper.GetString("ReCaptchaKey"),
ReCaptchaSecret: viper.GetString("ReCaptchaSecret"),
DefaultUser: &filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
Scope: viper.GetString("Scope"),
FileSystem: fileutils.Dir(viper.GetString("Scope")),
ViewMode: viper.GetString("ViewMode"),
},
Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope)
},
}
err = fm.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 = fm.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 = fm.Attach(jekyll); err != nil {
log.Fatal(err)
}
}
return h.Handler(fm)
}

73
doc.go Normal file
View File

@@ -0,0 +1,73 @@
/*
Package filemanager 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 Manager and File Manager HTTP packages.
import (
fm "github.com/hacdias/filemanager"
h "github.com/hacdias/filemanager/http"
)
Then, you should create a new FileManager 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/hacdias/filemanager/bolt".
db, _ := storm.Open("bolt.db")
m := &fm.FileManager{
NoAuth: false,
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 Manager'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 Manager 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 Manager 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 filemanager

137
file.go
View File

@@ -7,7 +7,6 @@ import (
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"errors"
"hash"
"io"
"io/ioutil"
@@ -23,13 +22,9 @@ import (
"github.com/gohugoio/hugo/parser"
)
var (
errInvalidOption = errors.New("Invalid option")
)
// 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).
// 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"`
@@ -54,37 +49,35 @@ type file struct {
// Stores the content of a text file.
Content string `json:"content,omitempty"`
*listing `json:",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 {
// 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.
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.
// 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"`
// Displays in mosaic or list.
Display string `json:"display"`
}
// getInfo gets the file information and, in case of error, returns the
// GetInfo gets the file information and, in case of error, returns the
// respective HTTP error code
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
func GetInfo(url *url.URL, c *FileManager, u *User) (*File, error) {
var err error
i := &file{
i := &File{
URL: "/files" + url.String(),
VirtualPath: url.Path,
Path: filepath.Join(string(u.FileSystem), url.Path),
Path: filepath.Join(u.Scope, url.Path),
}
info, err := u.FileSystem.Stat(url.Path)
@@ -106,11 +99,11 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
return i, nil
}
// getListing gets the information about a specific directory and its files.
func (i *file) getListing(c *RequestContext, r *http.Request) error {
// 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 := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0)
f, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
if err != nil {
return err
}
@@ -123,7 +116,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
}
var (
fileinfos []*file
fileinfos []*File
dirCount, fileCount int
)
@@ -134,12 +127,22 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
for _, f := range files {
name := f.Name()
allowed := c.User.Allowed("/" + name)
allowed := u.Allowed("/" + name)
if !allowed {
continue
}
if strings.HasPrefix(f.Mode().String(), "L") {
// It's a symbolic link
// The FileInfo from Readdir treats symbolic link as a file only.
info, err := os.Stat(f.Name())
if err != nil {
return err
}
f = info
}
if f.IsDir() {
name += "/"
dirCount++
@@ -150,7 +153,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
// Absolute URL
url := url.URL{Path: baseurl + name}
i := &file{
i := &File{
Name: f.Name(),
Size: f.Size(),
ModTime: f.ModTime(),
@@ -166,7 +169,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
fileinfos = append(fileinfos, i)
}
i.listing = &listing{
i.Listing = &Listing{
Items: fileinfos,
NumDirs: dirCount,
NumFiles: fileCount,
@@ -175,8 +178,8 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
return nil
}
// getEditor gets the editor based on a Info struct
func (i *file) getEditor() error {
// 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" {
@@ -194,7 +197,6 @@ func (i *file) getEditor() error {
// 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
}
@@ -205,7 +207,7 @@ func (i *file) getEditor() error {
// GetFileType obtains the mimetype and converts it to a simple
// type nomenclature.
func (i *file) GetFileType(checkContent bool) error {
func (i *File) GetFileType(checkContent bool) error {
var content []byte
var err error
@@ -258,14 +260,11 @@ func (i *file) GetFileType(checkContent bool) error {
// If the type isn't text (and is blob for example), it will check some
// common types that are mistaken not to be text.
for _, extension := range textExtensions {
if strings.HasSuffix(i.Name, extension) {
if isInTextExtensions(i.Name) {
i.Type = "text"
goto End
}
}
} else {
i.Type = "blob"
}
End:
// If the file type is text, save its content.
@@ -283,7 +282,8 @@ End:
return nil
}
func (i file) Checksum(kind string) (string, error) {
// 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
@@ -293,7 +293,7 @@ func (i file) Checksum(kind string) (string, error) {
var h hash.Hash
switch kind {
switch algo {
case "md5":
h = md5.New()
case "sha1":
@@ -303,7 +303,7 @@ func (i file) Checksum(kind string) (string, error) {
case "sha512":
h = sha512.New()
default:
return "", errInvalidOption
return "", ErrInvalidOption
}
_, err = io.Copy(h, file)
@@ -315,12 +315,12 @@ func (i file) Checksum(kind string) (string, error) {
}
// CanBeEdited checks if the extension of a file is supported by the editor
func (i file) CanBeEdited() bool {
func (i File) CanBeEdited() bool {
return i.Type == "text"
}
// ApplySort applies the sort order using .Order and .Sort
func (l listing) ApplySort() {
func (l Listing) ApplySort() {
// Check '.Order' to know how to sort
if l.Order == "desc" {
switch l.Sort {
@@ -349,10 +349,10 @@ func (l listing) ApplySort() {
}
}
// Implement sorting for listing
type byName listing
type bySize listing
type byModified listing
// Implement sorting for Listing
type byName Listing
type bySize Listing
type byModified Listing
// By Name
func (l byName) Len() int {
@@ -411,21 +411,38 @@ func (l byModified) Less(i, j int) bool {
return iModified.Sub(jModified) < 0
}
var textExtensions = [...]string{
".md", ".markdown", ".mdown", ".mmark",
".asciidoc", ".adoc", ".ad",
".rst",
".json", ".toml", ".yaml", ".csv", ".xml", ".rss", ".conf", ".ini",
".tex", ".sty",
".css", ".sass", ".scss",
".js",
".html",
".txt", ".rtf",
".sh", ".bash", ".ps1", ".bat", ".cmd",
".php", ".pl", ".py",
// 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",
".c", ".cc", ".h", ".hh", ".cpp", ".hpp", ".f90",
".f", ".bas", ".d", ".ada", ".nim", ".cr", ".java", ".cs", ".vala", ".vapi",
}
// 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

View File

@@ -1,60 +1,9 @@
// Package filemanager 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 create a filemanager instance:
//
// m, err := filemanager.New(database, user)
//
// Where 'user' contains the default options for new users. You can just
// use 'filemanager.DefaultUser' or create yourself a default user:
//
// m, err := filemanager.New(database, filemanager.User{
// Admin: false,
// AllowCommands: false,
// AllowEdit: true,
// AllowNew: true,
// Commands: []string{
// "git",
// },
// Rules: []*filemanager.Rule{},
// CSS: "",
// FileSystem: webdav.Dir("/path/to/files"),
// })
//
// 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 Manager'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 Manager 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 Manager 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", m)
package filemanager
import (
"crypto/rand"
"errors"
"fmt"
"log"
"net/http"
"os"
@@ -64,38 +13,49 @@ import (
"strings"
"time"
rice "github.com/GeertJohan/go.rice"
"github.com/asdine/storm"
"golang.org/x/crypto/bcrypt"
"github.com/GeertJohan/go.rice"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/robfig/cron"
)
const (
// Version is the current File Manager version.
Version = "1.4.4"
ListViewMode = "list"
MosaicViewMode = "mosaic"
)
var (
errUserExist = errors.New("user already exists")
errUserNotExist = errors.New("user 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")
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")
)
// FileManager is a file manager instance. It should be creating using the
// 'New' function and not directly.
type FileManager struct {
// The BoltDB database for this instance.
db *storm.DB
// Cron job to manage schedulings.
Cron *cron.Cron
// The key used to sign the JWT tokens.
key []byte
Key []byte
// The static assets.
assets *rice.Box
Assets *rice.Box
// Job cron.
cron *cron.Cron
// 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 Manager as a middleware
@@ -111,174 +71,130 @@ type FileManager struct {
// there will only exist one user, called "admin".
NoAuth bool
// staticgen is the name of the current static website generator.
staticgen string
// ReCaptcha Site key and secret.
ReCaptchaKey string
ReCaptchaSecret string
// StaticGen is the static websit generator handler.
StaticGen StaticGen
// The Default User needed to build the New User page.
DefaultUser *User
// Users is a map with the different configurations for each user.
Users map[string]*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 *FileManager, u *User) error
// User contains the configuration for each user.
type User struct {
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// FSBuilder is the File System Builder.
type FSBuilder func(scope string) FileSystem
// Username is the user username used to login.
Username string `json:"username" storm:"index,unique"`
// The hashed password. This never reaches the front-end because it's temporarily
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// FileSystem is the virtual file system the user has access.
FileSystem fileutils.Dir `json:"filesystem"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
}
// 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
}
// DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
FileSystem: fileutils.Dir("."),
}
// New creates a new File Manager instance. If 'database' file already
// exists, it will load the users from there. Otherwise, a new user
// will be created using the 'base' variable. The 'base' User should
// not have the Password field hashed.
func New(database string, base User) (*FileManager, error) {
// Setup loads the configuration from the database and configures
// the Assets and the Cron job. It must always be run after
// creating a File Manager object.
func (m *FileManager) Setup() error {
// Creates a new File Manager instance with the Users
// map and Assets box.
m := &FileManager{
Users: map[string]*User{},
cron: cron.New(),
assets: rice.MustFindBox("./assets/dist"),
}
// Tries to open a database on the location provided. This
// function will automatically create a new one if it doesn't
// exist.
db, err := storm.Open(database)
if err != nil {
return nil, err
}
m.Assets = rice.MustFindBox("./assets/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 = db.Get("config", "key", &m.key)
if err != nil && err == storm.ErrNotFound {
err := m.Store.Config.Get("key", &m.Key)
if err != nil && err == ErrNotExist {
var bytes []byte
bytes, err = generateRandomBytes(64)
bytes, err = GenerateRandomBytes(64)
if err != nil {
return nil, err
return err
}
m.key = bytes
err = db.Set("config", "key", m.key)
m.Key = bytes
err = m.Store.Config.Save("key", m.Key)
}
if err != nil {
return nil, err
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 = db.Get("config", "commands", &m.Commands)
if err != nil && err == storm.ErrNotFound {
m.Commands = map[string][]string{
"before_save": {},
"after_save": {},
"before_publish": {},
"after_publish": {},
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
}
err = db.Set("config", "commands", m.Commands)
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 nil, err
return err
}
// Tries to fetch the users from the database and if there are
// any, add them to the current File Manager instance.
var users []User
err = db.All(&users)
if err != nil {
return nil, err
}
for i := range users {
m.Users[users[i].Username] = &users[i]
// 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 {
u := base
u := *m.DefaultUser
u.Username = "admin"
// Hashes the password.
u.Password, err = hashPassword("admin")
u.Password, err = HashPassword("admin")
if err != nil {
return nil, err
return err
}
// The first user must be an administrator.
@@ -289,25 +205,26 @@ func New(database string, base User) (*FileManager, error) {
u.AllowPublish = true
// Saves the user to the database.
if err := db.Save(&u); err != nil {
return nil, err
if err := m.Store.Users.Save(&u); err != nil {
return err
}
}
m.Users[u.Username] = &u
// TODO: remove this after 1.5
for _, user := range users {
if user.ViewMode != ListViewMode && user.ViewMode != MosaicViewMode {
user.ViewMode = ListViewMode
m.Store.Users.Update(user, "ViewMode")
}
}
// Attaches db to this File Manager instance.
m.db = db
m.DefaultUser.Username = ""
m.DefaultUser.Password = ""
// Create the default user, making a copy of the base.
base.Username = ""
base.Password = ""
m.DefaultUser = &base
m.Cron.AddFunc("@hourly", m.ShareCleaner)
m.Cron.Start()
m.cron.AddFunc("@hourly", m.shareCleaner)
m.cron.Start()
return m, nil
return nil
}
// RootURL returns the actual URL where
@@ -334,95 +251,32 @@ func (m *FileManager) SetBaseURL(url string) {
m.BaseURL = strings.TrimSuffix(url, "/")
}
// ServeHTTP handles the request.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := serveHTTP(&RequestContext{
FileManager: m,
User: nil,
File: nil,
}, w, r)
if code >= 400 {
w.WriteHeader(code)
if err == nil {
txt := http.StatusText(code)
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
w.Write([]byte(txt))
}
}
if err != nil {
log.Print(err)
w.Write([]byte(err.Error()))
}
}
// EnableStaticGen attaches a static generator to the current File Manager
// instance.
func (m *FileManager) EnableStaticGen(data StaticGen) error {
if reflect.TypeOf(data).Kind() != reflect.Ptr {
// Attach attaches a static generator to the current File Manager.
func (m *FileManager) Attach(s StaticGen) error {
if reflect.TypeOf(s).Kind() != reflect.Ptr {
return errors.New("data should be a pointer to interface, not interface")
}
if h, ok := data.(*Hugo); ok {
return m.enableHugo(h)
}
if j, ok := data.(*Jekyll); ok {
return m.enableJekyll(j)
}
return errors.New("unknown static website generator")
}
func (m *FileManager) enableHugo(h *Hugo) error {
if err := h.find(); err != nil {
err := s.Setup()
if err != nil {
return err
}
m.staticgen = "hugo"
m.StaticGen = h
m.StaticGen = s
err := m.db.Get("staticgen", "hugo", h)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "hugo", *h)
err = m.Store.Config.Get("staticgen_"+s.Name(), s)
if err == ErrNotExist {
return m.Store.Config.Save("staticgen_"+s.Name(), s)
}
return nil
}
func (m *FileManager) enableJekyll(j *Jekyll) error {
if err := j.find(); 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...)
}
m.staticgen = "jekyll"
m.StaticGen = j
err := m.db.Get("staticgen", "jekyll", j)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "jekyll", *j)
}
return nil
}
// shareCleaner removes sharing links that are no longer active.
// ShareCleaner removes sharing links that are no longer active.
// This function is set to run periodically.
func (m FileManager) shareCleaner() {
var links []shareLink
func (m FileManager) ShareCleaner() {
// Get all links.
err := m.db.All(&links)
links, err := m.Store.Share.Gets()
if err != nil {
log.Print(err)
return
@@ -431,7 +285,7 @@ func (m FileManager) shareCleaner() {
// Find the expired ones.
for i := range links {
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
err = m.db.DeleteStruct(&links[i])
err = m.Store.Share.Delete(links[i].Hash)
if err != nil {
log.Print(err)
}
@@ -439,39 +293,8 @@ func (m FileManager) shareCleaner() {
}
}
// 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
}
// 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)
}
// Runner runs the commands for a certain event type.
func (m FileManager) Runner(event string, path string) error {
func (m FileManager) Runner(event string, path string, destination string, user *User) error {
commands := []string{}
// Get the commands from the File Manager instance itself.
@@ -496,7 +319,15 @@ func (m FileManager) Runner(event string, path string) error {
}
cmd := exec.Command(command, args...)
cmd.Env = append(os.Environ(), "file="+path)
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
cmd.Env = append(cmd.Env, fmt.Sprintf("ROOT=%s", string(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
@@ -518,3 +349,217 @@ func (m FileManager) Runner(event string, path string) error {
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"`
// Username is the user username used to login.
Username string `json:"username" storm:"index,unique"`
// The hashed password. This never reaches the front-end because it's temporarily
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// Scope is the path the user has access to.
Scope string `json:"filesystem"`
// FileSystem is the virtual file system the user has access.
FileSystem FileSystem `json:"-"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// Prevents the user to change its password.
LockPassword bool `json:"lockPassword"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
// 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 {
*FileManager
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,49 +0,0 @@
package filemanager
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hacdias/fileutils"
)
type test struct {
*FileManager
Temp string
}
func (t test) Clean() {
t.db.Close()
os.RemoveAll(t.Temp)
}
func newTest(t *testing.T) *test {
temp, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatalf("Error creating temporary directory: %v", err)
}
scope := filepath.Join(temp, "scope")
database := filepath.Join(temp, "database.db")
err = fileutils.CopyDir("./testdata", scope)
if err != nil {
t.Fatalf("Error copying the test data: %v", err)
}
user := DefaultUser
user.FileSystem = fileutils.Dir(scope)
fm, err := New(database, user)
if err != nil {
t.Fatalf("Error creating a file manager instance: %v", err)
}
return &test{
FileManager: fm,
Temp: temp,
}
}

View File

@@ -1,27 +1,66 @@
package filemanager
package http
import (
"crypto/rand"
"encoding/json"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
fm "github.com/hacdias/filemanager"
)
// authHandler proccesses the authentication for the user.
func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
const reCaptchaAPI = "https://www.google.com/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(secret string, response string) (bool, error) {
body := url.Values{}
body.Set("secret", secret)
body.Add("response", response)
client := &http.Client{}
resp, err := client.Post(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"`
ChallengeTS time.Time `json:"challenge_ts"`
Hostname string `json:"hostname"`
ErrorCodes interface{} `json:"error-codes"`
}
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 *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// NoAuth instances shouldn't call this method.
if c.NoAuth {
return 0, nil
}
// Receive the credentials from the request and unmarshal them.
var cred User
var cred cred
if r.Body == nil {
return http.StatusForbidden, nil
}
@@ -31,14 +70,26 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
return http.StatusForbidden, nil
}
// Checks if the user exists.
u, ok := c.Users[cred.Username]
// If ReCaptcha is enabled, check the code.
if len(c.ReCaptchaSecret) > 0 {
ok, err := reCaptcha(c.ReCaptchaSecret, 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 !checkPasswordHash(cred.Password, u.Password) {
if !fm.CheckPasswordHash(cred.Password, u.Password) {
return http.StatusForbidden, nil
}
@@ -48,7 +99,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
// 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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func renewAuthHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
ok, u := validateAuth(c, r)
if !ok {
return http.StatusForbidden, nil
@@ -60,23 +111,21 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// claims is the JWT claims.
type claims struct {
User
NoAuth bool `json:"noAuth"`
fm.User
jwt.StandardClaims
}
// printToken prints the final JWT token to the user.
func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
func printToken(c *fm.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 := User{}
u := fm.User{}
u = *c.User
u.Password = ""
// Builds the claims.
claims := claims{
u,
c.NoAuth,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
Issuer: "File Manager",
@@ -85,7 +134,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
// Creates the token and signs it.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(c.key)
signed, err := token.SignedString(c.Key)
if err != nil {
return http.StatusInternalServerError, err
@@ -119,15 +168,16 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
// validateAuth is used to validate the authentication and returns the
// User if it is valid.
func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
func validateAuth(c *fm.Context, r *http.Request) (bool, *fm.User) {
if c.NoAuth {
c.User = c.DefaultUser
return true, c.User
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
return c.key, nil
return c.Key, nil
}
var claims claims
token, err := request.ParseFromRequestWithClaims(r,
extractor{},
@@ -139,38 +189,11 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
return false, nil
}
u, ok := c.Users[claims.User.Username]
if !ok {
u, err := c.Store.Users.Get(claims.User.ID, c.NewFS)
if err != nil {
return false, nil
}
c.User = u
return true, u
}
// 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 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,4 +1,4 @@
package filemanager
package http
import (
"io"
@@ -9,28 +9,21 @@ import (
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
"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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("format")
func downloadHandler(c *fm.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 {
if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline")
} else {
w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name)
}
http.ServeFile(w, r, c.File.Path)
return 0, nil
return downloadFileHandler(c, w, r)
}
query := r.URL.Query().Get("format")
files := []string{}
names := strings.Split(r.URL.Query().Get("files"), ",")
@@ -106,7 +99,29 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
defer file.Close()
w.Header().Set("Content-Disposition", "attachment; filename="+name)
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
_, err = io.Copy(w, file)
return 0, err
}
func downloadFileHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline")
} else {
w.Header().Set("Content-Disposition", `attachment; filename="`+c.File.Name+`"`)
}
file, err := os.Open(c.File.Path)
defer file.Close()
if err != nil {
return http.StatusInternalServerError, err
}
_, err = io.Copy(w, file)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}

View File

@@ -1,29 +1,45 @@
package filemanager
package http
import (
"encoding/json"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
// RequestContext contains the needed information to make handlers work.
type RequestContext struct {
*FileManager
User *User
File *file
// On API handlers, Router is the APi handler we want.
Router string
// Handler returns a function compatible with http.HandleFunc.
func Handler(m *fm.FileManager) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
code, err := serve(&fm.Context{
FileManager: 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"))
}
// serveHTTP is the main entry point of this HTML application.
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if err != nil {
log.Print(err)
}
})
}
// serve is the main entry point of this HTML application.
func serve(c *fm.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 error because we're not supposed to be here!
// returns a 404 fm.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 != "" {
@@ -35,11 +51,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
// 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,
c.assets.MustString("sw.js"),
"application/javascript",
)
return renderFile(c, w, "sw.js")
}
// Checks if this request is made to the static assets folder. If so, and
@@ -67,7 +79,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return c.StaticGen.Preview(c, w, r)
}
if strings.HasPrefix(r.URL.Path, "/share/") && c.StaticGen != nil {
if strings.HasPrefix(r.URL.Path, "/share/") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
return sharePage(c, w, r)
}
@@ -77,29 +89,21 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
w.Header().Set("x-content-type", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block")
return renderFile(
c, w,
c.assets.MustString("index.html"),
"text/html",
)
return renderFile(c, w, "index.html")
}
// staticHandler handles the static assets path.
func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil
}
return renderFile(
c, w,
c.assets.MustString("static/manifest.json"),
"application/json",
)
return renderFile(c, w, "static/manifest.json")
}
// apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth/get" {
return authHandler(c, w, r)
}
@@ -135,9 +139,9 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
if c.Router == "checksum" || c.Router == "download" {
var err error
c.File, err = getInfo(r.URL, c.FileManager, c.User)
c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
}
@@ -169,11 +173,11 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
}
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo")
val, err := c.File.Checksum(query)
if err == errInvalidOption {
if err == fm.ErrInvalidOption {
return http.StatusBadRequest, err
} else if err != nil {
return http.StatusInternalServerError, err
@@ -201,14 +205,39 @@ func splitURL(path string) (string, string) {
}
// renderFile renders a file using a template with some needed variables.
func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentType string) (int, error) {
tpl := template.Must(template.New("file").Parse(file))
func renderFile(c *fm.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")
err := tpl.Execute(w, map[string]interface{}{
data := map[string]interface{}{
"BaseURL": c.RootURL(),
"StaticGen": c.staticgen,
})
"NoAuth": c.NoAuth,
"Version": fm.Version,
"CSS": template.CSS(c.CSS),
"ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "",
"ReCaptchaKey": c.ReCaptchaKey,
"ReCaptchaSecret": c.ReCaptchaSecret,
}
if c.StaticGen != nil {
data["StaticGen"] = c.StaticGen.Name()
}
err := tpl.Execute(w, data)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -216,15 +245,12 @@ func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentTy
return 0, nil
}
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", r.URL.Path, &s)
if err == storm.ErrNotFound {
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
// sharePage build the share page.
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
s, err := c.Store.Share.Get(r.URL.Path)
if err == fm.ErrNotExist {
w.WriteHeader(http.StatusNotFound)
return renderFile(c, w, "static/share/404.html")
}
if err != nil {
@@ -232,22 +258,20 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
}
if s.Expires && s.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&s)
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
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 {
return errorToHTTP(err, false), err
c.Store.Share.Delete(s.Hash)
return ErrorToHTTP(err, false), err
}
c.File = &file{
c.File = &fm.File{
Path: s.Path,
Name: info.Name(),
ModTime: info.ModTime(),
@@ -259,7 +283,7 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
dl := r.URL.Query().Get("dl")
if dl == "" || dl == "0" {
tpl := template.Must(template.New("file").Parse(c.assets.MustString("static/share/index.html")))
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{}{
@@ -299,8 +323,8 @@ func matchURL(first, second string) bool {
return strings.HasPrefix(first, second)
}
// errorToHTTP converts errors to HTTP Status Code.
func errorToHTTP(err error, gone bool) int {
// ErrorToHTTP converts errors to HTTP Status Code.
func ErrorToHTTP(err error, gone bool) int {
switch {
case err == nil:
return http.StatusOK

View File

@@ -1,4 +1,4 @@
package filemanager
package http
import (
"errors"
@@ -13,6 +13,7 @@ import (
"strings"
"time"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
)
@@ -26,7 +27,7 @@ func sanitizeURL(url string) string {
return path
}
func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
@@ -36,8 +37,8 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return resourceDeleteHandler(c, w, r)
case http.MethodPut:
// Before save command handler.
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
if err := c.Runner("before_save", path); err != nil {
path := filepath.Join(c.User.Scope, r.URL.Path)
if err := c.Runner("before_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
@@ -47,7 +48,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// After save command handler.
if err := c.Runner("after_save", path); err != nil {
if err := c.Runner("after_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
@@ -61,11 +62,11 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusNotImplemented, nil
}
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file.
f, err := getInfo(r.URL, c.FileManager, c.User)
f, err := fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
// If it's a dir and the path doesn't end with a trailing slash,
@@ -82,7 +83,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
// Tries to get the file type.
if err = f.GetFileType(true); err != nil {
return errorToHTTP(err, true), err
return ErrorToHTTP(err, true), err
}
// Serve a preview if the file can't be edited or the
@@ -96,23 +97,23 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
f.Kind = "editor"
// Tries to get the editor data.
if err = f.getEditor(); err != nil {
if err = f.GetEditor(); err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, f)
}
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func listingHandler(c *fm.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, r); err != nil {
return errorToHTTP(err, true), err
if err := f.GetListing(c.User, r); err != nil {
return ErrorToHTTP(err, true), err
}
listing := f.listing
listing := f.Listing
// Defines the cookie scope.
cookieScope := c.RootURL()
@@ -129,27 +130,35 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
}
listing.ApplySort()
listing.Display = displayMode(w, r, cookieScope)
return renderJSON(w, f)
}
func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourceDeleteHandler(c *fm.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
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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowNew && r.Method == http.MethodPost {
return http.StatusForbidden, nil
}
@@ -174,7 +183,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
// Otherwise we try to create the directory.
err := c.User.FileSystem.Mkdir(r.URL.Path, 0776)
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
// If using POST method, we are trying to create a new file so it is not
@@ -186,23 +195,28 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
}
}
// 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, 0776)
if err != nil {
return errorToHTTP(err, false), err
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
return ErrorToHTTP(err, false), err
}
// Gets the info about the file.
fi, err := f.Stat()
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
// Check if this instance has a Static Generator and handles publishing
@@ -217,10 +231,16 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
// 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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
publish := r.Header.Get("Publish")
schedule := r.Header.Get("Schedule")
@@ -241,7 +261,7 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
return http.StatusInternalServerError, err
}
c.cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
_, err := resourcePublish(c, w, r)
if err != nil {
log.Print(err)
@@ -251,11 +271,11 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
return http.StatusOK, nil
}
func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
func resourcePublish(c *fm.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); err != nil {
if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
@@ -265,7 +285,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Executed the before publish command.
if err := c.Runner("before_publish", path); err != nil {
if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
@@ -273,7 +293,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit {
return http.StatusForbidden, nil
}
@@ -282,7 +302,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
action := r.Header.Get("Action")
dst, err := url.QueryUnescape(dst)
if err != nil {
return errorToHTTP(err, true), err
return ErrorToHTTP(err, true), err
}
src := r.URL.Path
@@ -292,38 +312,34 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
}
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
}
// displayMode obtains the display mode from the Cookie.
func displayMode(w http.ResponseWriter, r *http.Request, scope string) string {
var displayMode string
// Checks the cookie.
if displayCookie, err := r.Cookie("display"); err == nil {
displayMode = displayCookie.Value
}
// If it's invalid, set it to mosaic, which is the default.
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
displayMode = "mosaic"
}
// Set the cookie.
http.SetCookie(w, &http.Cookie{
Name: "display",
Value: displayMode,
MaxAge: 31536000,
Path: scope,
Secure: r.TLS != nil,
})
return displayMode
return ErrorToHTTP(err, true), err
}
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',

View File

@@ -1,4 +1,4 @@
package filemanager
package http
import (
"bytes"
@@ -6,12 +6,14 @@ import (
"net/http"
"reflect"
fm "github.com/hacdias/filemanager"
"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"`
@@ -26,7 +28,7 @@ type option struct {
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, errEmptyRequest
return nil, fm.ErrEmptyRequest
}
// Parses the request body and checks if it's well formed.
@@ -38,13 +40,13 @@ func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request type is right.
if mod.What != "settings" {
return nil, errWrongDataType
return nil, fm.ErrWrongDataType
}
return mod, nil
}
func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func settingsHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "" && r.URL.Path != "/" {
return http.StatusNotFound, nil
}
@@ -60,11 +62,12 @@ func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
type settingsGetRequest struct {
CSS string `json:"css"`
Commands map[string][]string `json:"commands"`
StaticGen []option `json:"staticGen"`
}
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func settingsGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
@@ -72,6 +75,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
result := &settingsGetRequest{
Commands: c.Commands,
StaticGen: []option{},
CSS: c.CSS,
}
if c.StaticGen != nil {
@@ -93,7 +97,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return renderJSON(w, result)
}
func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func settingsPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
@@ -102,9 +106,10 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
if err != nil {
return http.StatusBadRequest, err
}
// Update the commands.
if mod.Which == "commands" {
if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil {
if err := c.Store.Config.Save("commands", mod.Data.Commands); err != nil {
return http.StatusInternalServerError, err
}
@@ -112,6 +117,16 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
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)
@@ -119,7 +134,7 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusInternalServerError, err
}
err = c.db.Set("staticgen", c.staticgen, c.StaticGen)
err = c.Store.Config.Save("staticgen_"+c.StaticGen.Name(), c.StaticGen)
if err != nil {
return http.StatusInternalServerError, err
}

View File

@@ -1,4 +1,4 @@
package filemanager
package http
import (
"encoding/hex"
@@ -8,18 +8,10 @@ import (
"strings"
"time"
"github.com/asdine/storm"
"github.com/asdine/storm/q"
fm "github.com/hacdias/filemanager"
)
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"`
}
func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func shareHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
@@ -34,14 +26,10 @@ func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
return http.StatusNotImplemented, nil
}
func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var (
s []*shareLink
path = filepath.Join(string(c.User.FileSystem), r.URL.Path)
)
err := c.db.Find("Path", path, &s)
if err == storm.ErrNotFound {
func shareGetHandler(c *fm.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 == fm.ErrNotExist {
return http.StatusNotFound, nil
}
@@ -51,37 +39,42 @@ func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
for i, link := range s {
if link.Expires && link.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&shareLink{Hash: link.Hash})
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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path)
var s shareLink
var s *fm.ShareLink
expire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit")
if expire == "" {
err := c.db.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&s)
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 := generateRandomBytes(32)
bytes, err := fm.GenerateRandomBytes(32)
if err != nil {
return http.StatusInternalServerError, err
}
str := hex.EncodeToString(bytes)
s = shareLink{
s = &fm.ShareLink{
Path: path,
Hash: str,
Expires: expire != "",
@@ -108,19 +101,16 @@ func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
s.ExpireDate = time.Now().Add(add)
}
err = c.db.Save(&s)
if err != nil {
if err := c.Store.Share.Save(s); err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, s)
}
func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)
if err == storm.ErrNotFound {
func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
s, err := c.Store.Share.Get(strings.TrimPrefix(r.URL.Path, "/"))
if err == fm.ErrNotExist {
return http.StatusNotFound, nil
}
@@ -128,7 +118,7 @@ func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusInternalServerError, err
}
err = c.db.DeleteStruct(&s)
err = c.Store.Share.Delete(s.Hash)
if err != nil {
return http.StatusInternalServerError, err
}

View File

@@ -1,4 +1,4 @@
package filemanager
package http
import (
"encoding/json"
@@ -9,7 +9,7 @@ import (
"strconv"
"strings"
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
type modifyRequest struct {
@@ -19,12 +19,12 @@ type modifyRequest struct {
type modifyUserRequest struct {
*modifyRequest
Data *User `json:"data"`
Data *fm.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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersHandler(c *fm.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 {
@@ -47,7 +47,7 @@ func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
// 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 error.
// contain a valid ID, it returns an fm.Error.
func getUserID(r *http.Request) (int, error) {
// Obtains the ID in string from the URL and converts
// it into an integer.
@@ -63,11 +63,11 @@ func getUserID(r *http.Request) (int, error) {
// getUser returns the user which is present in the request
// body. If the body is empty or the JSON is invalid, it
// returns an error.
func getUser(r *http.Request) (*User, string, error) {
// returns an fm.Error.
func getUser(c *fm.Context, r *http.Request) (*fm.User, string, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, "", errEmptyRequest
return nil, "", fm.ErrEmptyRequest
}
// Parses the request body and checks if it's well formed.
@@ -79,13 +79,14 @@ func getUser(r *http.Request) (*User, string, error) {
// Checks if the request type is right.
if mod.What != "user" {
return nil, "", errWrongDataType
return nil, "", fm.ErrWrongDataType
}
mod.Data.FileSystem = c.NewFS(mod.Data.Scope)
return mod.Data, mod.Which, nil
}
func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersGetHandler(c *fm.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)
@@ -93,15 +94,15 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Request for the listing of users.
if r.URL.Path == "/" {
users := []User{}
users, err := c.Store.Users.Gets(c.NewFS)
if err != nil {
return http.StatusInternalServerError, err
}
for _, user := range c.Users {
// Copies the user info and removes its
// password so it won't be sent to the
// front-end.
u := *user
for _, u := range users {
// Removes the user password so it won't
// be sent to the front-end.
u.Password = ""
users = append(users, u)
}
sort.Slice(users, func(i, j int) bool {
@@ -116,54 +117,52 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusInternalServerError, err
}
// Searches for the user and prints the one who matches.
for _, user := range c.Users {
if user.ID != id {
continue
u, err := c.Store.Users.Get(id, c.NewFS)
if err == fm.ErrExist {
return http.StatusNotFound, err
}
if err != nil {
return http.StatusInternalServerError, err
}
u := *user
u.Password = ""
return renderJSON(w, u)
}
// If there aren't any matches, return not found.
return http.StatusNotFound, errUserNotExist
}
func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/" {
return http.StatusMethodNotAllowed, nil
}
u, _, err := getUser(r)
u, _, err := getUser(c, r)
if err != nil {
return http.StatusBadRequest, err
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername
return http.StatusBadRequest, fm.ErrEmptyUsername
}
// Checks if filesystem isn't empty.
if u.FileSystem == "" {
return http.StatusBadRequest, errEmptyScope
// Checks if scope isn't empty.
if u.Scope == "" {
return http.StatusBadRequest, fm.ErrEmptyScope
}
// Checks if password isn't empty.
if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword
}
// The username, password and scope cannot be empty.
if u.Username == "" || u.Password == "" || u.FileSystem == "" {
return http.StatusBadRequest, errors.New("username, password or scope is empty")
return http.StatusBadRequest, fm.ErrEmptyPassword
}
// Initialize rules if they're not initialized.
if u.Rules == nil {
u.Rules = []*Rule{}
u.Rules = []*fm.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.
@@ -177,33 +176,31 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Checks if the scope exists.
if code, err := checkFS(string(u.FileSystem)); err != nil {
if code, err := checkFS(u.Scope); err != nil {
return code, err
}
// Hashes the password.
pw, err := hashPassword(u.Password)
pw, err := fm.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = pw
u.ViewMode = fm.MosaicViewMode
// Saves the user to the database.
err = c.db.Save(u)
if err == storm.ErrAlreadyExists {
return http.StatusConflict, errUserExist
err = c.Store.Users.Save(u)
if err == fm.ErrExist {
return http.StatusConflict, err
}
if err != nil {
return http.StatusInternalServerError, err
}
// Saves the user to the memory.
c.Users[u.Username] = u
// Set the Location header and return.
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
w.Header().Set("Location", "/settings/users/"+strconv.Itoa(u.ID))
w.WriteHeader(http.StatusCreated)
return 0, nil
}
@@ -231,7 +228,7 @@ func checkFS(path string) (int, error) {
return 0, nil
}
func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil
}
@@ -242,27 +239,19 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
// Deletes the user from the database.
err = c.db.DeleteStruct(&User{ID: id})
if err == storm.ErrNotFound {
return http.StatusNotFound, errUserNotExist
err = c.Store.Users.Delete(id)
if err == fm.ErrNotExist {
return http.StatusNotFound, fm.ErrNotExist
}
if err != nil {
return http.StatusInternalServerError, err
}
// Delete the user from the in-memory users map.
for _, user := range c.Users {
if user.ID == id {
delete(c.Users, user.Username)
break
}
}
return http.StatusOK, nil
}
func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func usersPutHandler(c *fm.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
@@ -280,21 +269,25 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Gets the user from the request body.
u, which, err := getUser(r)
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.NoAuth {
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
err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
if err != nil {
return http.StatusInternalServerError, err
}
c.User.ViewMode = u.ViewMode
err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale)
err = c.Store.Users.Update(c.User, "CSS", "Locale", "ViewMode")
if err != nil {
return http.StatusInternalServerError, err
}
@@ -305,16 +298,19 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Updates the Password.
if which == "password" {
if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword
return http.StatusBadRequest, fm.ErrEmptyPassword
}
pw, err := hashPassword(u.Password)
if id == c.User.ID && c.User.LockPassword {
return http.StatusForbidden, nil
}
c.User.Password, err = fm.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
c.User.Password = pw
err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
err = c.Store.Users.Update(c.User, "Password")
if err != nil {
return http.StatusInternalServerError, err
}
@@ -324,27 +320,27 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// If can only be all.
if which != "all" {
return http.StatusBadRequest, errInvalidUpdateField
return http.StatusBadRequest, fm.ErrInvalidUpdateField
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername
return http.StatusBadRequest, fm.ErrEmptyUsername
}
// Checks if filesystem isn't empty.
if u.FileSystem == "" {
return http.StatusBadRequest, errEmptyScope
if u.Scope == "" {
return http.StatusBadRequest, fm.ErrEmptyScope
}
// Checks if the scope exists.
if code, err := checkFS(string(u.FileSystem)); err != nil {
if code, err := checkFS(u.Scope); err != nil {
return code, err
}
// Initialize rules if they're not initialized.
if u.Rules == nil {
u.Rules = []*Rule{}
u.Rules = []*fm.Rule{}
}
// Initialize commands if not initialized.
@@ -353,22 +349,20 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Gets the current saved user from the in-memory map.
var suser *User
for _, user := range c.Users {
if user.ID == id {
suser = user
break
}
}
if suser == nil {
suser, err := c.Store.Users.Get(id, c.NewFS)
if err == fm.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 := hashPassword(u.Password)
pw, err := fm.HashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -380,17 +374,10 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Updates the whole User struct because we always are supposed
// to send a new entire object.
err = c.db.Save(u)
err = c.Store.Users.Update(u)
if err != nil {
return http.StatusInternalServerError, err
}
// If the user changed the username, delete the old user
// from the in-memory user map.
if suser.Username != u.Username {
delete(c.Users, suser.Username)
}
c.Users[u.Username] = u
return http.StatusOK, nil
}

View File

@@ -1,4 +1,4 @@
package filemanager
package http
import (
"bytes"
@@ -13,6 +13,7 @@ import (
"time"
"github.com/gorilla/websocket"
fm "github.com/hacdias/filemanager"
)
var upgrader = websocket.Upgrader{
@@ -26,8 +27,8 @@ var (
)
// command handles the requests for VCS related commands: git, svn and mercurial
func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for errors.
func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for fm.Errors.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return 0, err
@@ -81,7 +82,7 @@ func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, er
}
// Gets the path and initializes a buffer.
path := string(c.User.FileSystem) + "/" + r.URL.Path
path := c.User.Scope + "/" + r.URL.Path
path = filepath.Clean(path)
buff := new(bytes.Buffer)
@@ -91,7 +92,7 @@ func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, er
cmd.Stderr = buff
cmd.Stdout = buff
// Starts the command and checks for errors.
// Starts the command and checks for fm.Errors.
err = cmd.Start()
if err != nil {
return http.StatusInternalServerError, err
@@ -239,8 +240,8 @@ func parseSearch(value string) *searchOptions {
}
// search searches for a file or directory.
func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for errors.
func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for fm.Errors.
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return 0, err
@@ -269,7 +270,7 @@ func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, err
search = parseSearch(value)
scope := strings.TrimPrefix(r.URL.Path, "/")
scope = "/" + scope
scope = string(c.User.FileSystem) + scope
scope = c.User.Scope + scope
scope = strings.Replace(scope, "\\", "/", -1)
scope = filepath.Clean(scope)

View File

@@ -10,61 +10,63 @@
},
"dependencies": {
"clipboard": "^1.7.1",
"codemirror": "^5.27.4",
"filesize": "^3.5.10",
"moment": "^2.18.1",
"codemirror": "^5.31.0",
"filesize": "^3.5.11",
"js-base64": "^2.4.0",
"moment": "^2.19.2",
"normalize.css": "^7.0.0",
"vue": "^2.3.3",
"vue-i18n": "^7.1.0",
"vue-router": "^2.7.0",
"vuex": "^2.3.1"
"noty": "^3.1.4",
"vue": "^2.5.8",
"vue-i18n": "^7.3.4",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^7.1.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^2.0.1",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"eslint": "^4.3.0",
"autoprefixer": "^7.1.6",
"babel-core": "^6.26.0",
"babel-eslint": "^8.0.2",
"babel-loader": "^7.1.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1",
"babel-preset-stage-2": "^6.24.1",
"babel-register": "^6.26.0",
"chalk": "^2.3.0",
"connect-history-api-fallback": "^1.5.0",
"copy-webpack-plugin": "^4.2.1",
"css-loader": "^0.28.7",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-html": "^3.1.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.1.1",
"eslint-plugin-promise": "^3.4.0",
"eslint-loader": "^1.9.0",
"eslint-plugin-html": "^4.0.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"express": "^4.16.2",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.5",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"http-proxy-middleware": "^0.17.4",
"opn": "^5.1.0",
"optimize-css-assets-webpack-plugin": "^3.0.0",
"ora": "^1.2.0",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.3.0",
"rimraf": "^2.6.2",
"semver": "^5.4.1",
"shelljs": "^0.7.8",
"sw-precache-webpack-plugin": "^0.11.4",
"uglify-js": "^3.0.23",
"url-loader": "^0.5.8",
"vue-loader": "^13.0.2",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.3.3",
"webpack": "^3.4.1",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0",
"uglify-js": "^3.1.10",
"url-loader": "^0.6.2",
"vue-loader": "^13.5.0",
"vue-style-loader": "^3.0.3",
"vue-template-compiler": "^2.5.8",
"webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.9.1",
"webpack-dev-middleware": "^2.0.4",
"webpack-hot-middleware": "^2.20.0",
"webpack-merge": "^4.1.1",
"yml-loader": "^2.1.0"
},
"engines": {

25
publish.sh Normal file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -e
echo "Building assets"
./build.sh
echo "Updating version number to $1..."
sed -i "s|(untracked)|$1|g" filemanager.go
echo "Commiting..."
git add -A
git commit -m "chore: version $1"
git push
echo "Creating the tag..."
git tag "v$1"
git push --tags
echo "Commiting untracked version notice..."
sed -i "s|$1|(untracked)|g" filemanager.go
git add -A
git commit -m "chore: setting untracked version [ci skip]"
git push
echo "Done!"

View File

@@ -1 +1 @@
4b205fed13570c943ad67f9fc4db4f51aeb62cec
2ba96e4a87cf7fcd056a9d72052623c5286ec7f1

View File

@@ -1,4 +1,4 @@
package filemanager
package staticgen
import (
"errors"
@@ -10,6 +10,7 @@ import (
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/varutils"
)
@@ -17,15 +18,6 @@ var (
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
)
// StaticGen is a static website generator.
type StaticGen interface {
SettingsPath() string
Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
}
// Hugo is the Hugo static website generator.
type Hugo struct {
// Website root
@@ -66,8 +58,13 @@ func (h Hugo) SettingsPath() string {
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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func (h Hugo) Hook(c *fm.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 {
@@ -87,7 +84,8 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
return http.StatusForbidden, nil
}
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
filename := filepath.Clean(r.URL.Path)
filename = strings.TrimPrefix(filename, string(filepath.Separator))
archetype := r.Header.Get("archetype")
ext := filepath.Ext(filename)
@@ -110,8 +108,8 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
}
// Publish publishes a post.
func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
func (h Hugo) Publish(c *fm.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") {
@@ -127,7 +125,7 @@ func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Preview handles the preview path.
func (h *Hugo) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func (h *Hugo) Preview(c *fm.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("", "")
@@ -186,7 +184,8 @@ func (h Hugo) undraft(file string) error {
return nil
}
func (h *Hugo) find() error {
// Setup sets up the plugin.
func (h *Hugo) Setup() error {
var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil {
return err
@@ -194,114 +193,3 @@ func (h *Hugo) find() error {
return nil
}
// 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
}
// 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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
// Publish publishes a post.
func (j Jekyll) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), 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 *RequestContext, 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))
}
func (j *Jekyll) find() error {
var err error
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
return err
}
return nil
}
// 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
}

125
staticgen/jekyll.go Normal file
View File

@@ -0,0 +1,125 @@
package staticgen
import (
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
)
// 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 *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
// Publish publishes a post.
func (j Jekyll) Publish(c *fm.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 *fm.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
}

19
staticgen/staticgen.go Normal file
View File

@@ -0,0 +1,19 @@
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
}

0
testdata/.gitkeep vendored
View File