Compare commits

...

77 Commits

Author SHA1 Message Date
Henrique Dias
1527ca0c50 Build assets Fri Aug 4 16:38:35 GMTST 2017
Former-commit-id: 68b8596be01fba8888cbe2986f045a74974b7358 [formerly a9b16a017049aef7cc57a838de93e65bef0b98e2] [formerly ec326398476caa44b89d7afa087612888cde4181 [formerly 26d9fd98ff]]
Former-commit-id: 7abe14b5648fd8a6cbee3fdd5700433cfdde15a4 [formerly 20fe929de14f28e21bf9309c032d3f0758d8ab27]
Former-commit-id: 1640635ad843b9d1e180b6705a697b205562b209
2017-08-04 16:38:35 +01:00
Henrique Dias
0dee98b40a Add aria label; close #131
Former-commit-id: 8fc595933b73d635bbe20e018958343d9dd3e31b [formerly c056d2fa20bec942e89710ff469e12cb07a498a9] [formerly a455096212ba0cb70102a99c714d8ff3e1401f17 [formerly 1861645b04]]
Former-commit-id: 794d333ef3a2788c5e76f7c1a558af8be1137c20 [formerly ab8ca7057862e8919a7d01c3d71ecd70ba7b32f3]
Former-commit-id: 3430567351ec7425e167c4c128bb7316ff631eac
2017-08-04 16:36:27 +01:00
Henrique Dias
412ac9c9d6 Search results slightly prettier
Former-commit-id: 518b86ee61c4816e346824e36ca2b8afc5a887ae [formerly d0e4ab7a725aa0dcf35acc0965e685563d33590e] [formerly 3eeb7807675ab03f3eb3ba9a2fb0f612c1ae742d [formerly 522d50333f]]
Former-commit-id: 311dbbb9e90be32928fdca3087bae73e574610fd [formerly fbf64faddb2a189f675b4652cd170418bbbf48f7]
Former-commit-id: 834138c56639871c32ad7f393440aee6a7281a9a
2017-08-04 16:15:07 +01:00
Henrique Dias
9c2609995a close #186
Former-commit-id: 14c5044db7c81c03f27c4dbf0e7c9ba3a89cb03a [formerly 7d0fe81767e4b33c64ae7da7dd684ca8f30539c4] [formerly 815bd3a5d9770e79527de56e8d6b5d79f34cb291 [formerly 1234ee32e4]]
Former-commit-id: 2330876399f0e325dc554a08de2513b385e39a7a [formerly 638d4627e6418da2d4ebba046735bd39e8e63ae2]
Former-commit-id: d1569f07397e7f31c164080326a3a91724999778
2017-08-04 14:10:23 +01:00
Henrique Dias
68cb4ee980 Update version output
Former-commit-id: 0d8bbcef2b4cdb93fc7c4929b875eb920fc01a64 [formerly 8bf71ac7db4a94d47346bbe382b135f60bc00788] [formerly 1426e8119be5d07f0d2a759f30784a223d76208d [formerly b7887ed162]]
Former-commit-id: 3e0d9dff358868119c6d3b109896259b84a1e565 [formerly e15610b544cb000a0ce88392bd13949a578fd605]
Former-commit-id: 6497b3d602ac78be939abc70d6c337799e9d85d8
2017-08-04 13:35:21 +01:00
Henrique Dias
adc82dd85e Delete README.md
Former-commit-id: c32b7a3c7967a342832c19687420fc53176bdf46 [formerly b0dcce97677ad53314f0318819bca23646fcf770] [formerly 2f642c119a8148aa6b77e08efd0fd1b7e6d3b1a0 [formerly 004f6ef0a2]]
Former-commit-id: 07190b22fb025336b9a7cbc3cdba0f0eac32d301 [formerly b9deeb3285c5ddc978900766677a76d3b2294be5]
Former-commit-id: 97d26cdbe84585dbf3f008d7faa058579f31336f
2017-08-02 15:24:02 +01:00
Henrique Dias
4f3375ee8d NoAuth for Caddy plugins
Former-commit-id: 5b8306eb527c9f21d14736c97e67b8689e69d025 [formerly 42d1b7035277e06aecc9a812ea4d92022a36c2a6] [formerly 31905bebcc8f337bc8f6815425778bfc652f43e4 [formerly b5961b6b63]]
Former-commit-id: 44aa1bc7df6ffa633b333b75a5045c612db44aae [formerly d22761bb6f49156f7889e28f3af8edb439c7e023]
Former-commit-id: 5a8a899a97bb7ec4c9433723c7d53f87163cccac
2017-08-02 14:49:15 +01:00
Henrique Dias
bda4fc16eb Build assets
Former-commit-id: f27eb9056ef686638857ffc310db691e8d23db86 [formerly f62ff93c222958b0245474b4d638a219bc49a2a1] [formerly 8a6c0a6c9dc6f053afe0e80aec0537a854dc7eab [formerly a3710efe19]]
Former-commit-id: 2bdbfd870005c9bfd7c7c52e8a63190b301ff4ba [formerly d0e8885cf013cbaf5220e825df94c6601cc00dc3]
Former-commit-id: 96b2f47ccd70cdeb2cf5514e8da1fd6f735b1c37
2017-08-02 14:25:49 +01:00
Henrique Dias
aa219e701e Add option to use FM w/o login
Former-commit-id: fd6209370028490ac4766e61ebd87350e12efd22 [formerly 88b307e153445f3a22a38b5113f8a184d87dc75c] [formerly 7c5d74411dee7036012e734555763df009190de6 [formerly f572fc7837]]
Former-commit-id: 791eab82673bdd5b41947b20f444066bb9070406 [formerly 582a15ff9879494afac6893a675bab094973d5a4]
Former-commit-id: a936d8b2e15203541a2b0ff074b4146867352426
2017-08-02 14:10:05 +01:00
Toyo
2460fd4fae Update zh-cn.yaml (#185)
Former-commit-id: 0998b3d4ec15f013c72e3a31896164c09f8edfa4 [formerly 6fc5846558dab9e173e91acb2b662dd7a522ad99] [formerly c6b04824795f0726f77d27ab94b9d8723f2a08df [formerly 58a3edde40]]
Former-commit-id: c78c8425ff4a2312a1318ac78046c97715f295c8 [formerly 3c3032335fe8d823035669e14a8d6252bd159fc2]
Former-commit-id: a776a3f7c37c484d865cc8cbabf1b2adb4a3272d
2017-08-02 12:21:19 +01:00
Henrique Dias
d50bec8caa Internationalization (#183)
* update dependencies to latest version

* add mising dependencies

* Syntax updates and such

* Reorganize files and translate login to portuguese

* Add i18n to buttons

* Error translations and some bug fixes

* Add i18n to files

* i18n on prompts

* update search

* Prompts and Sidebar in

* i18n to the header

* Change to YAML

* alphabetical order

* # Add simplified Chinese language (#180)

* Add Simplified Chinese and sort by alphabet

* Add more text to translations

* API Updates

* Update zh_cn.yaml (#182)

* Api Upgrades

* Simplify api and clean zh_cn lang file

* Improve error logging

* Fix some route bugs and separate login styles

* better organization

* Fix bug on api

* Build assets Tue, Aug  1, 2017 11:32:23 AM

* Rename users path and fix bug scroll event

* Start Portuguese translation and file org

* Add more to the PT translation

* Add show

* Build assets Tue Aug  1 12:01:39 GMTST 2017

* Add locale to cofnig

* Update portuguese translation

* You can change the language :)

* :D

* Build assets Tue Aug  1 17:50:31 GMTST 2017

* Update requestContext variable names

* Remove assets

* Build assets Tue Aug  1 20:48:21 GMTST 2017


Former-commit-id: 08f373725c14990f61dbb00bea43118c496c5d32 [formerly 281e23007c79dac1e9b86424201891a99d20f73a] [formerly b1b73f42debbce06b4f36e4cf97e319789c85b9f [formerly d8bc73390c]]
Former-commit-id: 92e99405cbf9935d1cf77b0fe70b122fca552be6 [formerly 3cd365e862f2a54ada60e226a19ac607b8d0c43b]
Former-commit-id: cf9815114ac686cdf75a6b1cba15adafe493d083
2017-08-01 20:49:56 +01:00
Henrique Dias
a5a68a8944 Version flag; close #175; improve error logging
Fix #175

improve error logging

build assets


Former-commit-id: c0feee879514a18dabe649368ac16895acf71f5a [formerly 644474964c42dc7ac3684059baa72d4f974c3fc1] [formerly df7e47d330f713c717f04a66430c895dce6933e4 [formerly 24dbcb87ba]]
Former-commit-id: d864fca67e2c39ea0dd94a61ee89515de700284d [formerly fc3cba4628faaef6ae99562b7b236e82ccf20589]
Former-commit-id: dd20e74775af0e4e2232a2fda3dd1bba169ce165
2017-07-30 11:32:14 +01:00
Henrique Dias
2029ca9aa6 ignore freebsd and openbsd arm6
Former-commit-id: 0a97ea6650357c720dfafc0cd1e83cdadb612b1b [formerly f5989744ba906de9d7158253c06fda0d6fb73c96] [formerly 56288c3b3a1213ade7a0d19723518122ddadb3d5 [formerly f9bfe32f7a]]
Former-commit-id: a5fc489c1824efda3e2c91600711d2f1787ef4b4 [formerly 18e36218ee00c229db43897f102dac1f2895be8d]
Former-commit-id: 8dbe5c9a68203453de706fe8460274a68bd419f6
2017-07-29 20:04:31 +01:00
Henrique Dias
311b943e40 ...
Former-commit-id: fe2658e1833cb5d5dabcd3c24ee6f2e6638bf6c9 [formerly 53843d203844ce35fc0e15a206df57ec4fa2fb43] [formerly 3ab39a54f025c3104493ede34b8ecd66940352d4 [formerly 7f4a7616f2]]
Former-commit-id: dd36053010acbe54a02e83f25b8e714c13f645d6 [formerly ba4fa376782cc9611beb07b9ba2fee7482ad3a6e]
Former-commit-id: 1d833baf2636399b98c7f2984ba0db556ca61504
2017-07-29 19:57:10 +01:00
Henrique Dias
620344bf0e ignore opendsb arm6
Former-commit-id: 3b42cf43c1a9c76513d591351c09e4f6f5146d14 [formerly 57d4edea5d5e4878559ab0a935ff250a67c3cfa4] [formerly b94aebb2f6ab16b636544a593a9aaebfd7047529 [formerly aa6f06b0b3]]
Former-commit-id: 07f001258719fd2a80fbe95417fe68332d5c9702 [formerly 8ea0dee1f3fb5e2194a39f37c9073ce1bab2c886]
Former-commit-id: e0dabfae946623764de4dfc2771c96a227fcd5e6
2017-07-29 19:49:07 +01:00
Henrique Dias
7bb2d37661 Update go
Former-commit-id: 7fdfbb8a060347083dbdc09d75ff29e2d2f6e4fc [formerly 6488a2e3d0a7331695534c92618ba27d3a381755] [formerly b2f1e8a90aeac88f499fb05bf666a196313b05d8 [formerly 03dcfa0d08]]
Former-commit-id: 6ff6ccc26b92eb6233b25f20318847c5c0366c66 [formerly bd4c23fb1171d6ba1c8c410ebb7a210662ccea19]
Former-commit-id: d5d22720b2cd0a8e92ca337230142ef729ce63b1
2017-07-29 19:38:47 +01:00
Henrique Dias
e5f2331fb7 Update
Former-commit-id: 1740868ca4d3e278a4a80d26d6f20a0afc24262a [formerly a44fe0c8fbad8ebe0822ed9d9da489f9866841c7] [formerly bab3c6ac76f8053d1885537242ff3065d3d56827 [formerly c431645012]]
Former-commit-id: 2867b02316377a7cfe0ff7dc448e8cd2a1dd514b [formerly c40ec908fc39642b2776c6e895c9c98427bbdaae]
Former-commit-id: 3577f256d7d69889a6056a932ea76a9cf84dc856
2017-07-29 19:35:49 +01:00
Henrique Dias
f158b79f89 Deploy assets Sat, Jul 29, 2017 7:12:20 PM
Former-commit-id: e1df8e7645d5ea91d080ab346d3aea8db2fa8901 [formerly 1a89b0c2c46c734ff5ac68fbf774b8d734d9e32c] [formerly 4a6cdd29616db5ba9e193f205b64cc417da6e059 [formerly 282efb1f92]]
Former-commit-id: 92b1ed4d4cab52351a630362d3b04b32e0b49e05 [formerly 39526e23e52123332ba202bbf45b76baa784c96e]
Former-commit-id: 31041f02b3c9bca16b64a84b739b24a19f15322e
2017-07-29 19:12:20 +01:00
Henrique Dias
de9cf466c7 Try to fix Hugo
Former-commit-id: 7c530392c2696d200147ea878e6da0321888a52b [formerly 24028571fda891d65ebacba15317ec1f15d348ec] [formerly 9e210d5046e518fe37991711f9412d3390f3fbfe [formerly d20079bc4e]]
Former-commit-id: 39987e08efd3cb9af30043983f896bbea2299012 [formerly c1c6d86b39e23ccb6e9d9ffd10d88b8de7355e2a]
Former-commit-id: e45d76537a24cf5dcd978e687ddc5819ab4a4e60
2017-07-29 19:11:20 +01:00
Henrique Dias
b2f6f95916 Fix Hugo error
Former-commit-id: 22a3e0ab7462efcf920a14a8f45da27c0fd683dd [formerly c393472665da880feee876ca3c24e474660c1f44] [formerly b83bf6b70a23c6522a9896ef47819f0442c5d2c8 [formerly c0b60d5928]]
Former-commit-id: d3b03bb09b509b29781c0f585b2f44401f1f37cf [formerly e40c32c1eb119d2ccfa681b62310642534b71b79]
Former-commit-id: 1e643026d4fde9e223dbb29c108acfb530963624
2017-07-29 18:08:27 +01:00
Henrique Dias
ea2cc50a3f Update .travis.yml
Former-commit-id: 182cf6259d751c0065b12d9a4fec70bbccdef984 [formerly 469e198868e61a6356febe8f897d7ac40499f635] [formerly 30f788591952eb073da71f4660e7fe4b66327796 [formerly 9ca6ef1f3e]]
Former-commit-id: 5b39e4efa51d40273a5c03b7c84d28c4cec24c71 [formerly ab996ead9b59e6bfed5e7f340ece411a8c3e3af0]
Former-commit-id: 0d14515f250e54e4f686303e42b953965f509015
2017-07-29 15:47:01 +01:00
Henrique Dias
ef61957127 Update README.md
Former-commit-id: 9a0d8a56b49aeb2b345f268f2b17d8bb4bbcc42a [formerly 87bf607b3080a0542f044c5045b88ddb632d671d] [formerly 40d29f9c9066942f9c9ff7d106c7dbd43f8cb293 [formerly c42707bbd9]]
Former-commit-id: 2a67073f8ad99e4b5ae3ef1af40a074b76dd7d22 [formerly b2bc3e625eb8fab3f413123ae5c7721ae514163b]
Former-commit-id: 32cf32cedcba361df9935762fb5e49adc1b1dc0a
2017-07-29 15:37:23 +01:00
Henrique Dias
efe36312ec fix config flag
Former-commit-id: 65a11b9443bda4265bf1fd364152bc73b6f9c1bd [formerly 305a3ac513356904f617a7f676af0a24f985d402] [formerly 1e28fe6c8425f10b0bf0d095906cc3d7e7a37bb9 [formerly 2334bb601d]]
Former-commit-id: aca1773aca8a57a73c635990a4d41fba6e665736 [formerly 3e9b1d5cfff424d8b3d3c28687c885b77ab16587]
Former-commit-id: badcc38a9615287bc232a0aa3fb70310d16afe3c
2017-07-29 14:57:45 +01:00
Henrique Dias
c5fbb47147 Update auth
Former-commit-id: fb2387557f3225b19f4e66d3aab57e307d65bb8f [formerly 6770133ffae5bb2ba23863807002d726d3b75d54] [formerly f92b0039f3462b535c714243b56df9308c714fb6 [formerly e09bd03eec]]
Former-commit-id: 2e52e1ec0bf55421333fd3dd3b07b90d6a8a5aa6 [formerly f4ac0c99c3e4b35f094b2fe78f59e8bcab612c44]
Former-commit-id: f3a7282fdc551ebabbe1a9bf334ce4c08a525dd6
2017-07-29 14:32:07 +01:00
Henrique Dias
6602735030 Deploy assets Sat, Jul 29, 2017 2:29:35 PM
Former-commit-id: 068cb3fd903a8a2625a4bf0fa8cf270ac7944c81 [formerly 2c93624b534ba4fe617a4cb3c540cfcb7c358f14] [formerly 4eb29756b7fba1d0d5119c647c8dc92d6b941f91 [formerly fe530a5bf2]]
Former-commit-id: fcd54b63b3d1e3bd21542f694dc569e808620d89 [formerly 74a31e128b3cde0512229499a7702d3bd348c5f8]
Former-commit-id: 9008d3e636ae53e739f293a70e110cc2c83cbf06
2017-07-29 14:29:35 +01:00
Henrique Dias
eaafbb5dba Deploy assets Sat Jul 29 14:28:14 GMTST 2017
Former-commit-id: a51b80d1895d6269e98c1110535ab00751a77846 [formerly f0a14195e674f64ad573f1617e3b2a4036f49e20] [formerly f4fa02476c22cecc94d9fc8a54cf90364f22beed [formerly 3dd06a89b9]]
Former-commit-id: 55edc6f8cb3f056849d6587d53227ca5a8e992f7 [formerly 211d9d759a67bd156fbe076a698cd8a7374b1b8a]
Former-commit-id: 4d63fb0a4629835a1b9bc4bf6a25c76199eed225
2017-07-29 14:28:14 +01:00
Henrique Dias
33a99cf60c Deploy Assets Sat Jul 29 14:27:18 GMTST 2017
Former-commit-id: 07ba13627e2f83f6ed17fc91a6359f1287abcfbb [formerly 964b3a51d989df9e6cec63662ccc705809dd0369] [formerly 6b003c7a39837beb99937bfb6575243c0f23eeba [formerly c08ac26529]]
Former-commit-id: 334468d19504c41cdb1b84d055ab740280933242 [formerly 8930cf1486b83df73a4f1a45d5a9c6b14141ff62]
Former-commit-id: 96ed722026ce3368ba65506031bf03020e949e8e
2017-07-29 14:27:18 +01:00
Henrique Dias
0a4e119bc5 Close #172
Former-commit-id: d89982644eb2fde52436a4709227b3c68477bad0 [formerly ecef4fca1df20baff25c87edde6a3165953946e0] [formerly acd3363cf88346c69564ce902d8470a55b2865f0 [formerly 011a154f1f]]
Former-commit-id: 925e264736253dbcc1be8e7c31de9ebf2e5d2975 [formerly 23310a7c1fb9208d6519955194b31623bb927209]
Former-commit-id: 85e9ae5be30392151a709322c3574ac91dc5b52c
2017-07-29 14:26:33 +01:00
Henrique Dias
16c8b86241 Update dev script
Former-commit-id: 25adf086ba5834e03e88de4c2d15df247de01ba6 [formerly 1f9229558908f1077bfd79d7c4aabff447985784] [formerly 0741c0f731e538fea85fa5ca47792e5f252af5b2 [formerly 114ee2f9a2]]
Former-commit-id: 1f3dbc3d9d57a9ba4f36e8fd621a8f12e297473b [formerly d0a5aaf28933ab24100b4b266b9d78e710e34215]
Former-commit-id: 62187ca89633afeb47f854efa86fc90bc5076c56
2017-07-29 14:21:26 +01:00
Henrique Dias
49c8dfa12e Move docs to front page
Former-commit-id: 37efe1a9a6729b52a82e5d338b9659a1e4bef0a9 [formerly 170b050a47c16ff8f506784213608dac372993ad] [formerly 59b53b63807434c3e200d94e3c97a9beeda5ab33 [formerly afcfd1c62f]]
Former-commit-id: b421b435acd4c2a78f5b8211e521741e586cac1b [formerly 3e295bdd761fd4bd3fc7082dad09d504893be41f]
Former-commit-id: a9a4f3fc6fef5725dbe4a2f4b1f7aaf6ee006653
2017-07-29 14:20:58 +01:00
Henrique Dias
5c2fda6b4a Add Content Type to print jwt; #170
Former-commit-id: 133e076b5d480db7529acf2ff21b7e6ae9426b68 [formerly e5401526831ca33d37d0f69598d174383eb96f02] [formerly 85f1d51200d8b533e3e38b486ca0313671d3e2d3 [formerly 4df76b79c7]]
Former-commit-id: bd2eb54eb6f7ae863c1e73181d27bbb7be7b2e43 [formerly bad56e0155cf17d17052e02ef91913b487914845]
Former-commit-id: 45f136c96ad024186a51c0285fbe7f5557877ad0
2017-07-29 14:19:09 +01:00
Henrique Dias
1d3be7cf60 Use Viper for Configuration
Former-commit-id: b2c8a0b401ba3f1d482265c420988838bf4b9c84 [formerly adefdb8687decca47bb11c43cda8c2a596d5759e] [formerly 68f5399796e1e1f152c40a38664c8600a96ceec9 [formerly aa6631c408]]
Former-commit-id: 5c08d6dc2fcdca74ec48af22b53a87cf9237d7a9 [formerly 08974d1b71804e789f64a57c19f5fc1f0a4cb930]
Former-commit-id: d6580b1675682ae116f00b468cd7232c15115111
2017-07-29 13:28:18 +01:00
Henrique Dias
b8daa19d8f Logging #154
Former-commit-id: 28310c27bb3268a74c69da4f68cb7093a109e737 [formerly 3e8989eb196ac24a8ae2812ef71dd336e6595c72] [formerly 69f335f059f7cacc341cab185dfd536723796664 [formerly adfc66896d]]
Former-commit-id: e44b11858f2258373bd7da18f88acacaf3056c69 [formerly 020986a6b6f9c2180feb58c0d0494b2a8c047245]
Former-commit-id: a1731a44f91418bb5e5d8f8376062e2d44556423
2017-07-29 11:53:34 +01:00
Henrique Dias
c6b392f1fb build assets
Former-commit-id: 0c39c6fd3727777a2de193ff1b17efeee0180d62 [formerly 199a5577b3bb9a2fc32d22484fd9e5418bd90efa] [formerly cbc9118c385062fe6807b0296022835fb65725ac [formerly c89875d454]]
Former-commit-id: 3dce6a9f5de2b054dc3701eeb0ff2b0fff341298 [formerly c5a25784954c2e3d36defd5065e9e6a88194cb6f]
Former-commit-id: a0289d6a25c333b7b03952bd18b80e874ddbbd1e
2017-07-29 11:38:40 +01:00
Henrique Dias
75e4afc1cb Update build
Former-commit-id: 8ffbf3c07714f5fc59dc83d96271d67d8ca338e1 [formerly 1e674babe8fed63770242209f28afbd29d484d6e] [formerly d694c1b656ce0969f5e1a8cf891dcb321afafbf5 [formerly 22bfa7094a]]
Former-commit-id: 8bea921628838cc7d73a99f07070e0e0cf103d43 [formerly 11e677d99558349148f374a65f15f7ef12b0f295]
Former-commit-id: f20db50513e25a6e13aa13c2dba7a21105ad3a2d
2017-07-29 11:33:01 +01:00
Henrique Dias
3c9762ee97 Better Plugin Parsing
Former-commit-id: 8f0d86770072714575223249ab59cb0bcb11d1af [formerly 745c8318de08dc06bd9eece0d185528e4ed0872c] [formerly 61cda90693313822ad2e106dbc5a64104f2b5716 [formerly 722ca4b47f]]
Former-commit-id: c2dad3e81d44f3a92c61d3c21eb6d2113b333ed4 [formerly 898013a275bf0e2b86395f3fa8d5479b08c692d8]
Former-commit-id: 6e7c0f06a65cecdb2307df35db361f24d2353e00
2017-07-29 11:32:07 +01:00
Henrique Dias
5968111f3e Working at least
Former-commit-id: ec78da37b86f172b600dd5568196e0d3515d806d [formerly 3728442230a2e87402eca56380ef3858d14d87f2] [formerly d714694ae051f1f10b0e815d68a9e49eadfc89b7 [formerly 46fdd869a5]]
Former-commit-id: 05a4f1eba2f0c8d7c6f4591805222195eb849144 [formerly c813a9d09fbe2c30bda1e1ad135bdd3b35cc61f6]
Former-commit-id: 12f1a7003f0a0c6c514bd72f1eee20c0e0f4453f
2017-07-29 11:02:18 +01:00
Henrique Dias
00be85db13 Updates; instable
Former-commit-id: e3f4a16286e135cb643f36fc9518a760dca82333 [formerly ffd101f980b04f10bc4bc99e43bcc3ff962993a4] [formerly 15e574f9edbd1579a12424d51aa9926e0dbd84dd [formerly ae8e97a43e]]
Former-commit-id: a2db52a130b73a1ac235630d87797edd0c38c756 [formerly bf85e68f90f75cad611b90545ce7415123de84bd]
Former-commit-id: f9915ecf27cc6c2456eb867a7d3a8beb29531e35
2017-07-29 10:54:05 +01:00
Henrique Dias
0d453229d9 Hugo Updates
Former-commit-id: 35d71feaf1921d3b965331e02ac45274a24e1d5f [formerly 98ad98d2326fee3f4dc318f0597f53688704ecfe] [formerly 0d02b487fd32a169a4f1133e086058f1db3df108 [formerly b863033d7a]]
Former-commit-id: 3fc286e691e56c3ff03b0e50ea89adf586a86789 [formerly 2b4bbc4ce6bab8c69c9baa3653480221a1ff71d2]
Former-commit-id: 83d23b6b60d00b6499729e145074396c7ffbc06d
2017-07-29 10:14:34 +01:00
Eric Volpert
4a4db4f4ee Last Modified Sort (#174)
* Last Modified sorting

* Goimported the file so travis doesn't complain.


Former-commit-id: 06484c47f1426eb26be91032bc8269a74db2c75b [formerly 45a2fa0c020ec678e8780a7b1877013cccd709fd] [formerly 576db545eb2caee9497fa01cac64b95a9c73e8f7 [formerly b61a989958]]
Former-commit-id: 574cce1c8c2dc2680149f829f491a45561ec08e1 [formerly 4aed04767c209ab1fe2d43b44eecdfcc15554552]
Former-commit-id: e95f9822151173fe30713e749a3a510d792d3636
2017-07-29 07:57:44 +01:00
Henrique Dias
1755d52019 Update readme
Former-commit-id: e88efbe384ea35b4a19c4547e6323e00ca0ee960 [formerly c18bfec3508f09554b4453cffb3bc29825cf2d07] [formerly 2ee6a01866e7e460525d499379d21defb1f85850 [formerly 94ab0297f9]]
Former-commit-id: 7ad31383405484b0b95a166ca75e9fadad49a709 [formerly 5ae4d839ec0ef1c1e81a8592fc57474c0546b594]
Former-commit-id: bcc1b0c070c77eb61a6df6159556bd0552e2d2d4
2017-07-28 21:07:29 +01:00
Henrique Dias
ea72ff6990 Update README.md
Former-commit-id: 0abf0bd844b948422ff1bbb9f4c1cd434084e7d7 [formerly bda2c712e757fbf042865b101a608ff8370e7284] [formerly eac6bcec2e873c8faccae66cbe8d101d25906adc [formerly f080b657d5]]
Former-commit-id: 28f8e19f3def680985cb748e5f33bd483e1e01fd [formerly 819e07eb37c1f0ba51b1e567e989099a88934a93]
Former-commit-id: dfe471c9c000e42914893edfde1f547dbc927fc7
2017-07-28 14:34:59 +01:00
Henrique Dias
8777050d3e Update README.md
Former-commit-id: 22fba43b4459ee9f6b957a474ca20428778258cf [formerly c6ad29099fb71bd1674737ae84b7e50277cef3c9] [formerly 210afa8e9ed15d4fe59848cb773aad757173d513 [formerly 5c7a0b61d3]]
Former-commit-id: 0aff06fc85f31cf342989ba3c24562cff7a2b06c [formerly 0118f40e7c2ffb9c174df43ba24ef9b11f1f8977]
Former-commit-id: ee823f96ce2ab93a91cd1e5456ee89687f95e100
2017-07-28 14:34:15 +01:00
Henrique Dias
ae860e5bfc Create CONTRIBUTING.md
Former-commit-id: 9a0b7f42558604211f1209c78b45476c80613ffb [formerly 0418a1c0690581069c4749c794d44f87da82298e] [formerly d46e205f0349a58c33538437dba8a716d7212b73 [formerly 9d9eaf03a3]]
Former-commit-id: 72538667e655928cc4b460ae3fb458780578bb71 [formerly caa99ae829c154ad1e4ec8f69b9e4c5d1e51be77]
Former-commit-id: 9cf73b9b63a8c80f0dd26b1307e6c7a8eb6152a0
2017-07-28 14:33:49 +01:00
thomasyxy
7ce9c72dc5 docs: update README (#167)
Former-commit-id: e1b33419721d6d51a851fcc7ea562a050b40f9de [formerly 03a0aea84db533381cc79dd458c1cdf4d9fdad2c] [formerly 97fc9c300c4798a92386707d0a7af1223e8f2c60 [formerly cc84429746]]
Former-commit-id: 5aa11704c1cf2c5f45e2a81ca7d51b9ae10ea953 [formerly a2513d93485c729f94479695e760b817efb59d78]
Former-commit-id: db848231e7ae321798ee47bfa87bff05052f4d70
2017-07-28 14:03:54 +01:00
Henrique Dias
37282b0721 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 47141758dff2997ec7d26e6653b0ae143ceb2d4e [formerly acee6e41b8a27bb214de8ec621aed76b34e5ddcf] [formerly 17a6c77973fa8f3de7a866305e4c2aca18742567 [formerly dd648286d5]]
Former-commit-id: 711d695a556bd14d920884a4513b44b9b0d641fd [formerly 59b5f0990ef2d9f94fa9f355f71afa645f37cf13]
Former-commit-id: 4494ad60fb912998f13427bd9673f98c215be252
2017-07-28 11:23:26 +01:00
Fábio Ferreira
22fbc7d02d Better experience (#166)
* Making directory navigation prettier

* Small improvements

* Rice-box


Former-commit-id: fff23ea4a4aeff16e650571b1e50ac556db55e64 [formerly 1395942367664c8b8cac5181a44089e54002b061] [formerly d9afe630bc365cc227da4f18f76a98fe3f1843d9 [formerly 39a6b7de3c]]
Former-commit-id: 1d53b93648223d539d24512ea62b0d8ef3e07531 [formerly ee5428b9cd7a1cb3105f5dc42d4dce99584faf66]
Former-commit-id: 2a5352afece16ce936f736cc5a2edcdb208f8671
2017-07-28 11:22:44 +01:00
Henrique Dias
4905fc3800 remove notice from readme
Former-commit-id: 5f010954d64650bebdee3ca9b4e321dfd7bf7cdf [formerly 2603003f6777961aef3355a37bba3e66148dc95c] [formerly 51a4e9942edcbf620edf1f46cdfb59750be1abcb [formerly b9c389f5a0]]
Former-commit-id: 0467ec83ad5a4f5e32d75fd69f14ddd88cf37782 [formerly 5ae8d45466650c5fc7bdb64b4866d3d7380969e1]
Former-commit-id: 253384b3050b4f72de834b4c72828a85a60335dd
2017-07-28 11:22:17 +01:00
Henrique Dias
242b868a56 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: b1b0de09da7b47528a692adad39cd667a3da1d2f [formerly f3bf194faaf969fec1dab7cffcaa575e95e08e51] [formerly 16a66d90f3e0f913ea118bd9ac70272fee336900 [formerly 05b7b8aa0b]]
Former-commit-id: 733fd18a03d6919e6c895cf75c1ace68a4ae9314 [formerly bd425bdbf3c6f63b31618f472581ef1e31d1eca9]
Former-commit-id: bc3fc22aa86fb253b0106bacad73accfbc291081
2017-07-28 11:22:04 +01:00
Henrique Dias
e3c1e11b88 update build.sh
Former-commit-id: 97a6b53ace70ca68880ebc2dc5b5fafd8e8f12a8 [formerly 27d7c953391bafe9bb1139413953419bf87f73a9] [formerly 60a6c7bf2606edfbb2968885db90186926365a5e [formerly 81b1e8d430]]
Former-commit-id: 5c76a0d95d78292c1ef046320f1d889b0caf349e [formerly fe536d54d494d38c38c79465af5acefc91ddbdb0]
Former-commit-id: 8901073c535bc9f1537402fd565324e638a757f3
2017-07-28 11:21:55 +01:00
Henrique Dias
3cee6b67f0 Add Docker
Former-commit-id: dcb37f7c8649e689e93a5cbec639245cb66e3b05 [formerly 4a0678e53f804e2898df22b95cb2b192a5e481b8] [formerly 4435baab269f8142b49fdbc75c437fc14cc67839 [formerly aaa254689f]]
Former-commit-id: ce01f9194f26cdfd3b68edf5a59da33a4b9f2d37 [formerly 67c11145731e1171f1ab30ab3a632498dcb73594]
Former-commit-id: a2549d020b96c251bf002f30530df8f25cf03609
2017-07-28 11:04:31 +01:00
Henrique Dias
7f46ef9a97 Add go rice notice
Former-commit-id: 1cf9a6f790a167f00e35090d78b3ce6d96d2eb1b [formerly b85aabf46fd0582b8f4004f7be97cd33fa97e0cb] [formerly c28c70f3279ad7231eb8449ffac451d91773eaa6 [formerly 775d06bda7]]
Former-commit-id: 67e1adb78c8361ac89e88b3a4706ead16394be8c [formerly d683d4465cd25c5cd03dde2b117f6591a6d5a3f6]
Former-commit-id: b48c0ea8454366294e0209dbb636ee18f4720767
2017-07-28 11:01:48 +01:00
Henrique Dias
25c20aaeb2 Build assets
Former-commit-id: b00f47f672956abd67217d3bfcc9681c53a5cb21 [formerly 13efcc0784356df1eef3f85fc64637cf1a46ea23] [formerly 8f4a7fc9c6199c7454ab920deaf2eb50cdd80bbb [formerly 05a6ebe805]]
Former-commit-id: b09670b87308aad975716f56578964a28352171a [formerly 3e2909f91026e82933fd476ec571915211613df4]
Former-commit-id: 0622c1396b8c94ae2342c87263d3006dd80cba06
2017-07-28 10:04:46 +01:00
Henrique Dias
4c665ec69c Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 32ee5895db96495db099b05391ad775b64e91ec3 [formerly 4636b9d22f781d91167e45a39930448b10d85e43] [formerly 5ae6fd1b6b8894b85c0df81d7e53d8e787d4797c [formerly 537c839d9e]]
Former-commit-id: 99579a9a68807d7fa55e9abfa8b2881f509b4ce2 [formerly d48f9c64d5dddada4f0e71ce611fc75c93477ec4]
Former-commit-id: 5a20514a2e2d250796b271c14ec90de022beb725
2017-07-28 09:59:36 +01:00
Henrique Dias
75ed779b17 Use varutils instead
Former-commit-id: 454971910d12915f6668454ab00c0f92f9f80f40 [formerly 5fd29b6b99f57f9cd6be8a5c09151ef3feabbcc9] [formerly ac7480dcb9b5d3c240163f5f406f0ecebc842cee [formerly 2d83da4d7e]]
Former-commit-id: 77ec28f8a63a9b994059d28f467697cc6f03288b [formerly d031877a753306d0dd42c58686538a9eaf3211fc]
Former-commit-id: 818e4d03e50e4a04f1f38001c48dd45e71ed91ef
2017-07-28 09:59:31 +01:00
Henrique Dias
6c360ffc5c Update Dockerfile
Former-commit-id: ecae8322b52dc294e4926f542059b2a2f76d67e1 [formerly c0000943385ed1417a7e748d36f1cd25750d8217] [formerly 2a70f9a07f83e017b58f65c9dbe8583e7dc92227 [formerly 8117eb98d8]]
Former-commit-id: 992d5e311c66506d0e7ca90f49f38e4be7983afb [formerly 52ab26d2c1029266e6ddac201bc34bcb3271b984]
Former-commit-id: a4c5a02eee252ab728f4da55a6ed32d6470d7b81
2017-07-28 08:06:44 +01:00
Henrique Dias
f40731d3b4 Use fileutils instead
Former-commit-id: 2688c5dca3865614edaf7b159b7f0dcebad77171 [formerly b919610fb1d09db5e6b6308e5adc0d431ce6c139] [formerly b7c60fcf5771ec9d1854947dd89dc51ff414bdb7 [formerly ff034f7e8f]]
Former-commit-id: a63a598c5cef6c050a8a0d189bc5235a98d5457a [formerly cab7076a63efddbfb968c151c5ef1d34b00dac56]
Former-commit-id: f167b30eeea3a9c0b8fec43dc37ee95cd8c172e6
2017-07-27 21:39:23 +01:00
Henrique Dias
afb10cc02c Update and remove latest
Former-commit-id: 2585617bb0654bb6cfc32b7a64ebc8bf9a576ff0 [formerly 03a73d6f0ed0118e205e50ecab8d3bfc31a655b5] [formerly dddf658817bc25630be32647248d5e5533bcbc33 [formerly dac92eec8b]]
Former-commit-id: 77f85c483bf64ddc36013dd474100d459a6d599e [formerly ee7c07d1532d9bbd4c2d8e1428bb8a909f1f90b0]
Former-commit-id: b5b9dafcecc6990c70dfa0fffa071698736106b8
2017-07-27 18:15:51 +01:00
Henrique Dias
5d2e8864e5 A fix for #161
Former-commit-id: 73c31acfa1c812e896c57562338bd1fc27e3a3fb [formerly 15188c05c39a31691a875a57fadc2b397942146c] [formerly 6ad4d891a760572954b88383b41ced35bffc14c2 [formerly 97365f0e16]]
Former-commit-id: cb059d6f523e1f4a158bd504e04ce052ce32ae8b [formerly 945c4570eaa8478e7cea4dea8085a0ad69dfc063]
Former-commit-id: 138a1faefacab94f8ba14e0b72c75dd41fa9b965
2017-07-27 18:05:57 +01:00
Henrique Dias
d82f198b48 Delete _config.yml
Former-commit-id: 07bcc7620a718b01dfdbbe52d4ee0db6e20bd32e [formerly 1a0932293fabd2b19c0540fd6607c9bb9f68c153] [formerly 74ba54fee5c64ab93cd85720dd34c486a8a10a3a [formerly 703ee7daff]]
Former-commit-id: 14cbbe0ebe8a8ee0dd492c8b6b1035573a17dabf [formerly 4407d0442d0a32f8103ca86cb3f177b418b7cc84]
Former-commit-id: 6e4a79cb68099c567a42d75c95f2d8e912915893
2017-07-27 15:38:46 +01:00
Henrique Dias
bbbf2bac51 Set theme jekyll-theme-minimal
Former-commit-id: 9e9b72ead90fdd8f2cad943bd1cfe01c1a17d37e [formerly 14d56a8c62b54f0d2f90e2be62521aa4a34b220e] [formerly 041af3cd74e055fd89888c3406f59d92eb13117f [formerly 92c1a1622c]]
Former-commit-id: ae6bb20fb3fe17c430da8fd7e13a7e8f9194a228 [formerly 9eba2146635347aa33fcdde363bc81574a319ae3]
Former-commit-id: 421dd2e790dd8f0b647cd78685b16144e6ab1476
2017-07-27 15:28:39 +01:00
Henrique Dias
d87a8ee5ed Update README.md
Former-commit-id: f5d3de7a867ec129722eb713c96dc3c1a2dbd29d [formerly 13041237112a88c32a576965470f26829ef09e76] [formerly 1ec2ac3a1cb4ed8f851073c44bd1910478fe6600 [formerly bcd1f57836]]
Former-commit-id: 6ef873900c9a33aa180966237c801a540beaae3f [formerly 933ca735ad2ebb87dfbcd32c59b84586a962bf5c]
Former-commit-id: 85b221326e049dc2fd976681a5e93b0f01d1d8fd
2017-07-27 14:30:40 +01:00
Henrique Dias
a81cc1e4ca Add dockerignore
Former-commit-id: cd7095c90fda66881b8ce945f8392ab0a1f48a5b [formerly 767ddf6d86daf63eb6b02b5772ce389fbc6e1276] [formerly 0c5bfe13aaa28188a0e3c303b4cc06f148d5a5d7 [formerly c970227440]]
Former-commit-id: 60d4e4affdbd8d61d470f92328471b8867a2fae8 [formerly 0077bc004f73c8f626ff6566422374ab25d22a2e]
Former-commit-id: 99f9d4a4a5d038c5ad0174fd51acc3d0f3418dbc
2017-07-27 14:17:41 +01:00
Henrique Dias
6c5111fd6e Build assets
Former-commit-id: cc18830de1feb1bf095ee587c88650aef7e00eb6 [formerly ba8acc12a4d9efc42bb6d70d3fe64635a6e26474] [formerly 84bd969ed90d4adc2f1055edf432fb1b06590168 [formerly 42a3243113]]
Former-commit-id: 26ad676ccb988cbb75ed9737741bdc921310b9f6 [formerly 763d12b75f3ab22e3796e1f1c7b55ad83e3dbfc9]
Former-commit-id: c64850bca8bb9d473fc8de932a16bd379b867c88
2017-07-27 14:04:52 +01:00
Henrique Dias
e846c16336 Add Docker.json and update Dockerfile. See #157
Former-commit-id: 02381af5eb56c6d17416b187abac0c5b0067179b [formerly 4ee41157924a7ebbeefd31ddd55919317da6a4fe] [formerly 914325aefc9c3f622b3368289f3499c526943b56 [formerly 02c98905ae]]
Former-commit-id: 5c9112505577abeb82bb0e9f6f04081b63838dbf [formerly 82821dd7c2a3bd04d11148b43c45cbd8331e7138]
Former-commit-id: 121ad025499afafc25ca21e322783aa91ac6ebf2
2017-07-27 14:03:53 +01:00
Henrique Dias
dde01c6931 Make search reload the dir after closing itself
Former-commit-id: e9774417ce7c29eded0158ef18535fcad57ddc8a [formerly 1a644e3fd8e191aee49a12a660069b39b38e2c0b] [formerly c371da590eb27fbcf290e56933b279ea75384cee [formerly 9453e22559]]
Former-commit-id: aba05b88f61352b6e6afb5bf9af8c033ac8ba310 [formerly d011d5ee10fa055bc3ea35889dddb968b476eb83]
Former-commit-id: c1138ce78249872f442646d26807140a94824e0b
2017-07-27 14:03:14 +01:00
Henrique Dias
8b40dc9d19 Fix websockets on other ports than 80
Former-commit-id: 92fc9e1bd2c7590af98c5246a7e035a3ca8f3d6f [formerly 3cbab224088f675ab9474a78682db7956f90a39e] [formerly 395d28fc36802680a6a30065cad749101c256753 [formerly 736e7d6167]]
Former-commit-id: 5cfac7b10ccaf36c3969164df5af8dee4a27a9a5 [formerly 8e8283eba1ae1c5bf0da72927a775fdac2e3ef79]
Former-commit-id: 9a89cbe037984be71de125da051a92aa0d1636ab
2017-07-27 14:02:50 +01:00
Henrique Dias
63fb4c0c52 Fix other commands not working
Former-commit-id: aeff50d7041e0e411380db06789d431ccfcd849f [formerly 80fc2ef99d5bca4b98447d1218cea45178e973e9] [formerly 4b6b489ceefa94c96830439a1c3e2972641fe536 [formerly 01a9896b19]]
Former-commit-id: d16f51eff82db054324ac67d87d044ed040c680b [formerly 0b18874eeee9a6db3946113224593b431c44c0c8]
Former-commit-id: 65e557d6703ca9d621da083c56caa7bb80ae089d
2017-07-27 13:34:55 +01:00
Henrique Dias
76047f4146 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 1643b508b73ce32c022df5135e76bab645c3e3c8 [formerly c07a0e73c64b358a659a2b5f9f4b94a40e1d488d] [formerly 61e888ede7349df3a40a6b3d83b3603459a5c7e7 [formerly 989b6a525d]]
Former-commit-id: 095032dc1c062ea090104fa1f4cd9d34332dc95f [formerly dd71906d3622b0a2b3763aca939be4e49c1160f6]
Former-commit-id: ef065a79e82a3f934925282b85bf4b4b45049668
2017-07-27 13:31:07 +01:00
Henrique Dias
fc4c6ec246 Don't throw error if it is not a draft
Former-commit-id: fb33f600de938ad16ac1c2f1ab6aaae30f5598a8 [formerly c98a595f1f9adb3fc9f792ef353fd09f2a93674d] [formerly bb7623d54853a8df0525313aafcf076c3114af26 [formerly 8a4e1b6520]]
Former-commit-id: 49ef204abe70d7b791260f589b9569a94c4cdca9 [formerly 9499193985f3b9d91a0e6fe25a028937df7e919b]
Former-commit-id: 7fdb34a388e878accf14ae24a7a53714c62eda06
2017-07-27 13:30:46 +01:00
Bitti
f5e93825d8 Fix link to github repo (#160)
Former-commit-id: 1bd1eaebb892163e93360f74a9321689dd3b0583 [formerly c5eea25d2d36a3f3f00f5eb5cd9e5b97f2498019] [formerly 2387782e054d0e1ffcc633bdafff5d6feef4e722 [formerly c90db487f7]]
Former-commit-id: 400ecf0884bc68fea1a1389236103013817959f7 [formerly f6dc144ea0b1fef8af13d519a13ce70aeedb66f4]
Former-commit-id: e9c3d71b1047a81b956407ceb36e8ee32b5b8c50
2017-07-27 13:17:16 +01:00
Henrique Dias
73d74394c8 Hopefully fix #159
Former-commit-id: fee7b5c6a519d5a6ff26b2a653a3c82984f1dead [formerly 7fee30ae26f6941db1b718b64adfa6250aaf82c3] [formerly 46a761a926a67c0d0f8f49e84369912eba1377ef [formerly fbd0cf0c14]]
Former-commit-id: 4bdbf9a31f827d8aa36bad9b89f27bd5a373b75c [formerly 7e747b4e27d6958ff8448b5e5e2fe6716821a9ff]
Former-commit-id: a30d3b204d318b1d84e8583d5bbdbc6761852e2a
2017-07-27 12:32:10 +01:00
Henrique Dias
3469d8c6e8 Update Dockerfile
Former-commit-id: e8e99ed19cabe5a571f279cb2441964cdb954da9 [formerly cdbfd41a0ac408328eed4dd4466f1a9e0736ed0b] [formerly 69fc163bec96cc39dd5f77d1a7620eb9b29be28a [formerly ef315082ed]]
Former-commit-id: f81fbaeeee62c3a6a1d554f9dfff514ca095cf31 [formerly e5728fe8be1bbf463bd0b6e1f9df04de70dc3f4d]
Former-commit-id: f250a6c9fff5f8f397d1f3a6d00cfe0e4be23862
2017-07-27 11:38:09 +01:00
田浩
be082c1075 Add Docker Support (#157)
* dep init

* add Dockerfile

* update Dockerfile: add rice

* remove npm build & rice

* remove dep

* remove dep in Dockerfile


Former-commit-id: 6e7595fdfd54ad0bd3284124b418ac90cd635b74 [formerly 4e07bdd0e28b5d9361465eeca41cf78480a5c9c6] [formerly 2440226e8514f0c7e249ff2476721f049e1c416e [formerly bfc4362ddc]]
Former-commit-id: 35123c0371d2f05834780f3a9586ebed062497a9 [formerly 45ab33769a72f70f6c6c2307cc16cecbe662d025]
Former-commit-id: 4da50772ad13e8caefd3fd9b0d21ff675e0f7c6a
2017-07-27 10:27:59 +01:00
Henrique Dias
f271b18d97 Update Travis file
Former-commit-id: f8ab226e6a2c6132038abf4ae4f1d36def9bbeaf [formerly e7cbe888773009190ac7f37e6e3793a098f8ce83] [formerly 3fee80d398724ad66ab9eef0276581b21e06ce7b [formerly 155b5c3c02]]
Former-commit-id: 693d10cfd6a6e7ad4e4b0b1eb5fedc2a5ffcd6b4 [formerly c63af660b310eb19349d686475e4c503303420ee]
Former-commit-id: ca8f8aff411eaad6271885e9d84ab3bfd42dcde8
2017-07-27 08:23:50 +01:00
Henrique Dias
a728d0352e UPDATE TRAVIS
Former-commit-id: ea054af3fd934aebe0c8e2eab55f49bea60b63b2 [formerly e33717ab8dfc9d4bb3f28bda09e4b946afc66772] [formerly 440ef03b98c191821bd41991e57014e121372078 [formerly ae06e99cd1]]
Former-commit-id: 45b1a7afbb504b58194658ee3c08d17c2ddd1c9d [formerly 30356885794a37ddd1ae497c3cc6bcb2d16c59ea]
Former-commit-id: a94890a592a8efc4b8b8824f27e9560c044d5c14
2017-07-27 08:12:02 +01:00
Henrique Dias
2096d7d048 Build assets
Former-commit-id: 55d3d4aefbe1b0c374b64cb9b43db04cc5e2917c [formerly cc1399c7d5c4f57c49bc63649161b0174ca6cff3] [formerly 415dd63cd4052b765cbe3e34623723cd5d016438 [formerly f4506d06db]]
Former-commit-id: d29bc22e2ca476bfad2b6af89a949988e32b2930 [formerly 5e093ef79c6ed70d88b2429598c65449f7ef3ffc]
Former-commit-id: e288357ab7acfa509de7ef1353761f7db03985c3
2017-07-26 18:36:48 +01:00
93 changed files with 2468 additions and 2069 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
assets/
testdata/
caddy/
.github/

29
.goreleaser.yml Normal file
View File

@@ -0,0 +1,29 @@
build:
main: cmd/filemanager/main.go
binary: filemanager
goos:
- darwin
- linux
- windows
- freebsd
- netbsd
- openbsd
goarch:
- amd64
- 386
- arm
- arm64
ignore:
- goos: openbsd
goarch: arm
goarm: 6
- goos: freebsd
goarch: arm
goarm: 6
archive:
name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}"
format: tar.gz
format_overrides:
- goos: windows
format: zip

View File

@@ -1,15 +1,12 @@
language: go
go:
- tip
go: 1.8.3
env:
- "PATH=/home/travis/gopath/bin:$PATH"
install:
- go get ./...
- go get github.com/mitchellh/gox
- go get github.com/tcnksm/ghr
# Install gometalinter and certain linters
- go get github.com/alecthomas/gometalinter
- go get github.com/client9/misspell/cmd/misspell
@@ -20,17 +17,6 @@ install:
script:
- gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --exclude="rice-box.go" --tests ./...
- go test ./... -timeout 30s
- cd cmd/filemanager
- mkdir dist
- gox -output "dist/{{.OS}}-{{.Arch}}-{{.Dir}}"
- ls dist/
- ghr --username hacdias --token $GITHUB_TOKEN --replace --prerelease --debug latest dist/
deploy:
provider: releases
api_key: $GITHUB_TOKEN
file_glob: true
file: dist/*
skip_cleanup: true
on:
tags: true
after_success:
- test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash

14
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,14 @@
# Contributing
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`
2. `cd $GOPATH/src/github.com/hacdias/filemanager`
3. `npm install`
4. `npm run dev` - regenerates the static assets automatically
5. `go install github.com/hacdias/filemanager/cmd/filemanager`
6. Execute `$GOPATH/bin/filemanager`
The steps 3 and 4 are only required **if you want to develop the front-end**. Otherwise, you can ignore them. Before pulling, if you made any change on assets folder, you must run the `build.sh` script on the root of this repository.
If you are using this as a Caddy plugin, you should use its [official instructions for plugins](https://github.com/mholt/caddy/wiki/Extending-Caddy#2-plug-in-your-plugin) and import `github.com/hacdias/filemanager/caddy/filemanager`.

10
Docker.json Normal file
View File

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

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM golang:alpine
COPY . /go/src/github.com/hacdias/filemanager
WORKDIR /go/src/github.com/hacdias/filemanager
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 mv filemanager /go/bin/filemanager
FROM alpine:latest
COPY --from=0 /go/bin/filemanager /usr/local/bin/filemanager
VOLUME /srv
EXPOSE 80
COPY Docker.json /etc/config.json
ENTRYPOINT ["/usr/local/bin/filemanager"]
CMD ["--config", "/etc/config.json"]

View File

@@ -11,8 +11,6 @@ filemanager provides a file managing interface within a specified directory and
# Table of contents
+ [Getting started](#getting-started)
- [Caddy](#caddy)
- [Standalone](#standalone)
+ [Features](#features)
- [Users](#users)
- [Search](#search)
@@ -21,61 +19,7 @@ filemanager provides a file managing interface within a specified directory and
# Getting started
This is a library that can be used on your own applications as a middleware (see the [documentation](http://godoc.org/github.com/hacdias/filemanager)), as a plugin to Caddy web server or as a standalone app.
Once you have everything deployed, the default credentials to login to the filemanager are:
**Username:** `admin`
**Password:** `admin`
## Caddy
The easiest way to get started is using this with Caddy web server. You just need to download Caddy from its [official website](https://caddyserver.com/download) with `http.filemanager` plugin enabled. For more information about the plugin itself, please refer to its [documentation](https://caddyserver.com/docs/http.filemanager).
## Standalone
You can use filemanager as a standalone executable. You just need to download it from the [releases page](https://github.com/hacdias/filemanager/releases), where you can find multiple releases. The 'latest' always corresponds to the latest commit made to the master branch so it might not be stable.
You can either use flags or a JSON configuration file, which should have the following appearance:
```json
{
"port": 80,
"address": "127.0.0.1",
"database": "/path/to/database.db",
"scope": "/path/to/my/files",
"allowCommands": true,
"allowEdit": true,
"allowNew": true,
"commands": [
"git",
"svn"
]
}
```
The `scope`, `allowCommands`, `allowEdit`, `allowNew` and `commands` options are the defaults for new users. To set a configuration file, you will need to pass the path with a flag, like this: `filemanager --config=/path/to/config.json`.
Otherwise, you may not want to use a configuration file, which can be done using the following flags:
```
-address string
Address to listen to (default is all of them)
-allow-commands
Default allow commands option (default true)
-allow-edit
Default allow edit option (default true)
-allow-new
Default allow new option (default true)
-commands string
Space separated commands available for new users (default "git svn hg")
-database string
Database path (default "./filemanager.db")
-port string
HTTP Port (default is random)
-scope string
Default scope for new users (default ".")
```
You can find the Getting Started guide on the [documentation](https://henriquedias.com/filemanager/quick-start/).
# Features
@@ -127,17 +71,7 @@ this are keywords case:insensitive
# Contributing
If you want to contribute or want to build the code from source, you will need to have NodeJS and Go installed on your computer. You should start by doing the following:
```
go get github.com/hacdias/filemanager
```
Then, you should navigate to `$GOPATH/src/github.com/hacdias/filemanager` and execute `npm install`. You can start the live build of static assets with the command `npm start dev`.
If you are using this as a Caddy plugin, you should use its [official instructions for plugins](https://github.com/mholt/caddy/wiki/Extending-Caddy#2-plug-in-your-plugin) and import `github.com/hacdias/filemanager/caddy/filemanager`.
Before pulling, and if you made any change on assets folder, you must run the `build.sh` script on the root of this repository.
The contributing guidelines can be found [here](https://github.com/hacdias/filemanager/blob/master/CONTRIBUTING.md).
# Donate

View File

@@ -14,8 +14,8 @@ if (fs.existsSync('./rice-box.go')) {
fs.unlinkSync('./rice-box.go')
}
if (fs.existsSync('./caddy/hugo/rice-box.go')) {
fs.unlinkSync('./caddy/hugo/rice-box.go')
if (fs.existsSync('./plugins/rice-box.go')) {
fs.unlinkSync('./plugins/rice-box.go')
}
rm(path.join(config.assetsRoot, config.assetsSubDirectory), err => {

View File

@@ -25,6 +25,10 @@ module.exports = {
},
module: {
rules: [
{
test: /\.(yml|yaml)$/,
loader: 'yml-loader'
},
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',

View File

@@ -21,14 +21,13 @@
<!-- 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.chunks) {
<% for (var chunk of webpack.compilation.chunks) {
for (var file of chunk.files) {
if (file.match(/\.(js|css)$/)) { %>
<link rel="<%= chunk.initial?'preload':'prefetch' %>" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<!-- Plugins info -->
<script>{{ range $index, $plugin := .Plugins }}{{ JS $plugin.JavaScript }}{{ end}}</script>
<script>{{ .JavaScript }}</script>
<style>
#loading {
position: fixed;

View File

@@ -1,19 +1,19 @@
<template>
<header>
<div>
<button @click="openSidebar" aria-label="Toggle sidebar" title="Toggle sidebar" class="action">
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
<i class="material-icons">menu</i>
</button>
<img src="../assets/logo.svg" alt="File Manager">
<search></search>
</div>
<div>
<button @click="openSearch" aria-label="Search" title="Search" class="search-button action">
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
<i class="material-icons">search</i>
</button>
<button v-show="showSaveButton" aria-label="Save" class="action" id="save-button">
<i class="material-icons" title="Save">save</i>
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
<i class="material-icons">save</i>
</button>
<div v-for="plugin in plugins" :key="plugin.name">
@@ -30,7 +30,7 @@
</button>
</div>
<button @click="openMore" id="more" aria-label="More" title="More" class="action">
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
</button>
@@ -71,9 +71,9 @@
<upload-button v-show="showUpload"></upload-button>
<info-button v-show="showCommonButton"></info-button>
<button v-show="showSelectButton" @click="openSelect" aria-label="Select multiple" class="action">
<button v-show="showSelectButton" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
<i class="material-icons">check_circle</i>
<span>Select</span>
<span>{{ $t('buttons.select') }}</span>
</button>
</div>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
@@ -92,7 +92,7 @@ import SwitchButton from './buttons/SwitchView'
import MoveButton from './buttons/Move'
import CopyButton from './buttons/Copy'
import {mapGetters, mapState} from 'vuex'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {

View File

@@ -0,0 +1,19 @@
<template>
<select v-on:change="change" :value="selected">
<option value="en">{{ $t('languages.en') }}</option>
<option value="pt">{{ $t('languages.pt') }}</option>
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
</select>
</template>
<script>
export default {
name: 'languages',
props: [ 'selected' ],
methods: {
change (event) {
this.$emit('update:selected', event.target.value)
}
}
}
</script>

View File

@@ -1,118 +0,0 @@
<template>
<div id="login">
<form @submit="submit">
<img src="../assets/logo.svg" alt="File Manager">
<h1>File Manager</h1>
<div v-if="wrong" class="wrong">Wrong credentials</div>
<input type="text" v-model="username" placeholder="Username">
<input type="password" v-model="password" placeholder="Password">
<input type="submit" value="Login">
</form>
</div>
</template>
<script>
import auth from '@/utils/auth'
export default {
name: 'login',
data: function () {
return {
wrong: false,
username: '',
password: ''
}
},
methods: {
submit: function (event) {
event.preventDefault()
event.stopPropagation()
let redirect = this.$route.query.redirect
if (redirect === '' || redirect === undefined || redirect === null) {
redirect = '/files/'
}
auth.login(this.username, this.password)
.then(() => {
this.$router.push({ path: redirect })
})
.catch(() => {
this.wrong = true
})
}
}
}
</script>
<style>
#login {
background: #fff;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#login img {
width: 4em;
height: 4em;
margin: 0 auto;
display: block;
}
#login h1 {
text-align: center;
font-size: 2.5em;
margin: .4em 0 .67em;
}
#login form {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 16em;
width: 90%;
}
#login input {
width: 100%;
width: 100%;
margin: .5em 0 0;
}
#login .wrong {
background: #F44336;
color: #fff;
padding: .5em;
text-align: center;
animation: .2s opac forwards;
}
@keyframes opac {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
#login input[type="text"],
#login input[type="password"] {
padding: .5em 1em;
border: 1px solid #e9e9e9;
transition: .2s ease border;
color: #333;
}
#login input[type="text"]:focus,
#login input[type="password"]:focus,
#login input[type="text"]:hover,
#login input[type="password"]:hover {
border-color: #9f9f9f;
}
</style>

View File

@@ -1,82 +0,0 @@
<template>
<div class="dashboard">
<h1>Profile Settings</h1>
<ul v-if="user.admin">
<li><router-link to="/settings/global">Go to Global Settings</router-link></li>
</ul>
<form @submit="changePassword">
<h2>Change Password</h2>
<p><input :class="passwordClass" type="password" placeholder="Your new password" v-model="password" name="password"></p>
<p><input :class="passwordClass" type="password" placeholder="Confirm your new password" v-model="passwordConf" name="password"></p>
<p><input type="submit" value="Change Password"></p>
</form>
<form @submit="updateCSS">
<h2>Custom Stylesheet</h2>
<textarea v-model="css" name="css"></textarea>
<p><input type="submit" value="Update"></p>
</form>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import api from '@/utils/api'
export default {
name: 'settings',
data: function () {
return {
password: '',
passwordConf: '',
css: ''
}
},
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
},
methods: {
...mapMutations([ 'showSuccess' ]),
changePassword (event) {
event.preventDefault()
if (this.password !== this.passwordConf) {
return
}
api.updatePassword(this.password).then(() => {
this.showSuccess('Password updated!')
}).catch(e => {
this.$store.commit('showError', e)
})
},
updateCSS (event) {
event.preventDefault()
api.updateCSS(this.css).then(() => {
this.$store.commit('setUserCSS', this.css)
this.$emit('css-updated')
this.showSuccess('Styles updated!')
}).catch(e => {
this.$store.commit('showError', e)
})
}
}
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
<div id="input">
<button v-if="active" class="action" @click="close">
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
<i class="material-icons">arrow_back</i>
</button>
<i v-else class="material-icons">search</i>
@@ -11,7 +11,7 @@
ref="input"
:autofocus="active"
v-model.trim="value"
aria-label="Write here to search"
:aria-label="$t('search.writeToSearch')"
:placeholder="placeholder">
</div>
@@ -20,7 +20,11 @@
<span v-if="search.length === 0 && commands.length === 0">{{ text }}</span>
<ul v-else-if="search.length > 0">
<li v-for="s in search">
<router-link @click.native="close" :to="'./' + s">./{{ s }}</router-link>
<router-link @click.native="close" :to="'./' + s.path">
<i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span>
</router-link>
</li>
</ul>
@@ -47,7 +51,8 @@ export default {
ongoing: false,
scrollable: null,
search: [],
commands: []
commands: [],
reload: false
}
},
watch: {
@@ -57,12 +62,17 @@ export default {
// If the hover was search and now it's something else
// we should blur the input.
if (old === 'search' && val !== 'search') {
if (this.reload) {
this.$store.commit('setReload', true)
}
this.$refs.input.blur()
}
// If we are starting to show the search box, we should
// focus the input.
if (val === 'search') {
this.reload = false
this.$refs.input.focus()
}
}
@@ -72,10 +82,10 @@ export default {
// Placeholder value.
placeholder: function () {
if (this.user.allowCommands && this.user.commands.length > 0) {
return 'Search or execute a command...'
return this.$t('search.searchOrCommand')
}
return 'Search...'
return this.$t('search.search')
},
// The text that is shown on the results' box while
// there is no search result or command output to show.
@@ -86,16 +96,16 @@ export default {
if (this.value.length === 0) {
if (this.user.allowCommands && this.user.commands.length > 0) {
return `Search or use one of your supported commands: ${this.user.commands.join(', ')}.`
return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.`
}
return 'Type and press enter to search.'
this.$t('search.type')
}
if (!this.supported() || !this.user.allowCommands) {
return 'Press enter to search.'
return this.$t('search.pressToSearch')
} else {
return 'Press enter to execute.'
return this.$t('search.pressToExecute')
}
}
},
@@ -129,7 +139,7 @@ export default {
let pieces = this.value.split(' ')
for (let i = 0; i < this.user.commands.length; i++) {
if (pieces[0] === this.user.commands[0]) {
if (pieces[0] === this.user.commands[i]) {
return true
}
}
@@ -166,9 +176,9 @@ export default {
this.scrollable.scrollTop = this.scrollable.scrollHeight
},
(event) => {
this.reload = true
this.ongoing = false
this.scrollable.scrollTop = this.scrollable.scrollHeight
this.$store.commit('setReload', true)
}
)
@@ -178,10 +188,12 @@ export default {
// In case of being a search.
api.search(path, this.value,
(event) => {
let url = event.data
if (url[0] === '/') url = url.substring(1)
let response = JSON.parse(event.data)
if (response.path[0] === '/') {
response.path = response.path.substring(1)
}
this.search.push(url)
this.search.push(response)
this.scrollable.scrollTop = this.scrollable.scrollHeight
},
(event) => {

View File

@@ -1,19 +1,19 @@
<template>
<nav :class="{active}">
<router-link class="action" to="/files/" aria-label="My Files" title="My Files">
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
<i class="material-icons">folder</i>
<span>My Files</span>
<span>{{ $t('sidebar.myFiles') }}</span>
</router-link>
<div v-if="user.allowNew">
<button @click="$store.commit('showHover', 'newDir')" aria-label="New directory" title="New directory" class="action">
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
<i class="material-icons">create_new_folder</i>
<span>New folder</span>
<span>{{ $t('sidebar.newFolder') }}</span>
</button>
<button @click="$store.commit('showHover', 'newFile')" aria-label="New file" title="New file" class="action">
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
<i class="material-icons">note_add</i>
<span>New file</span>
<span>{{ $t('sidebar.newFile') }}</span>
</button>
</div>
@@ -24,22 +24,22 @@
</button>
</div>
<div>
<router-link class="action" to="/settings" aria-label="Settings" title="Settings">
<div v-if="!$store.state.user.noAuth">
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
<i class="material-icons">settings_applications</i>
<span>Settings</span>
<span>{{ $t('sidebar.settings') }}</span>
</router-link>
<button @click="logout" class="action" id="logout" aria-label="Log out" title="Logout">
<button @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
<i class="material-icons">exit_to_app</i>
<span>Logout</span>
<span>{{ $t('sidebar.logout') }}</span>
</button>
</div>
<p class="credits">
<span>Served with <a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-filemanager">File Manager</a>.</span>
<span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
<span v-for="plugin in plugins" :key="plugin.name" v-html="plugin.credits"><br></span>
<span><a @click="help">Help</a></span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
</p>
</nav>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<button @click="show" aria-label="Copy" title="Copy" class="action" id="copy-button">
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
<i class="material-icons">content_copy</i>
<span>Copy file</span>
<span>{{ $t('buttons.copyFile') }}</span>
</button>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<button @click="show" aria-label="Delete" title="Delete" class="action" id="delete-button">
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
<i class="material-icons">delete</i>
<span>Delete</span>
<span>{{ $t('buttons.delete') }}</span>
</button>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<button @click="download" aria-label="Download" title="Download" id="download-button" class="action">
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
<i class="material-icons">file_download</i>
<span>Download</span>
<span>{{ $t('buttons.download') }}</span>
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
</button>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<button title="Info" aria-label="Info" class="action" @click="show">
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
<i class="material-icons">info</i>
<span>Info</span>
<span>{{ $t('buttons.info') }}</span>
</button>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<button @click="show" aria-label="Move" title="Move" class="action" id="move-button">
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
<i class="material-icons">forward</i>
<span>Move file</span>
<span>{{ $t('buttons.moveFile') }}</span>
</button>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<button @click="show" aria-label="Rename" title="Rename" class="action" id="rename-button">
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
<i class="material-icons">mode_edit</i>
<span>Rename</span>
<span>{{ $t('buttons.rename') }}</span>
</button>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<button @click="change" aria-label="Switch View" title="Switch View" class="action" id="switch-view-button">
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
<i class="material-icons">{{ icon() }}</i>
<span>Switch view</span>
<span>{{ $t('buttons.switchView') }}</span>
</button>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<button @click="upload" aria-label="Upload" title="Upload" class="action" id="upload-button">
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
<i class="material-icons">file_upload</i>
<span>Upload</span>
<span>{{ $t('buttons.upload') }}</span>
</button>
</template>

View File

@@ -1,10 +1,10 @@
<template>
<form id="editor" :class="req.language">
<div v-if="hasMetadata" id="metadata">
<h2>Metadata</h2>
<h2>{{ $t('files.metadata') }}</h2>
</div>
<h2 v-if="hasMetadata">Body</h2>
<h2 v-if="hasMetadata">{{ $t('files.body') }}</h2>
</form>
</template>
@@ -123,7 +123,3 @@ export default {
}
}
</script>
<style>
</style>

View File

@@ -2,9 +2,9 @@
<div v-if="(req.numDirs + req.numFiles) == 0">
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>It feels lonely here...</span>
<span>{{ $t('files.lonely') }}</span>
</h2>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
</div>
<div v-else id="listing"
:class="req.display"
@@ -15,22 +15,39 @@
<div class="item header">
<div></div>
<div>
<p :class="{ active: nameSorted }" class="name" @click="sort('name')">
<span>Name</span>
<p :class="{ active: nameSorted }" class="name"
role="button"
tabindex="0"
@click="sort('name')"
:title="$t('files.sortByName')"
:aria-label="$t('files.sortByName')">
<span>{{ $t('files.name') }}</span>
<i class="material-icons">{{ nameIcon }}</i>
</p>
<p :class="{ active: !nameSorted }" class="size" @click="sort('size')">
<span>Size</span>
<p :class="{ active: sizeSorted }" class="size"
role="button"
tabindex="0"
@click="sort('size')"
:title="$t('files.sortBySize')"
:aria-label="$t('files.sortBySize')">
<span>{{ $t('files.size') }}</span>
<i class="material-icons">{{ sizeIcon }}</i>
</p>
<p class="modified">Last modified</p>
<p :class="{ active: modifiedSorted }" class="modified"
role="button"
tabindex="0"
@click="sort('modified')"
:title="$t('files.sortByLastModified')"
:aria-label="$t('files.sortByLastModified')">
<span>{{ $t('files.lastModified') }}</span>
<i class="material-icons">{{ modifiedIcon }}</i>
</p>
</div>
</div>
</div>
<h2 v-if="req.numDirs > 0">Folders</h2>
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
<div v-if="req.numDirs > 0">
<item v-for="(item, index) in req.items"
v-if="item.isDir"
@@ -45,7 +62,7 @@
</item>
</div>
<h2 v-if="req.numFiles > 0">Files</h2>
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
<div v-if="req.numFiles > 0">
<item v-for="(item, index) in req.items"
v-if="!item.isDir"
@@ -60,12 +77,12 @@
</item>
</div>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<div v-show="$store.state.multiple" :class="{ active: $store.state.multiple }" id="multiple-selection">
<p>Multiple selection enabled</p>
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" title="Clear" aria-label="Clear" class="action">
<i class="material-icons" title="Clear">clear</i>
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
<i class="material-icons">clear</i>
</div>
</div>
</div>
@@ -86,6 +103,12 @@ export default {
nameSorted () {
return (this.req.sort === 'name')
},
sizeSorted () {
return (this.req.sort === 'size')
},
modifiedSorted () {
return (this.req.sort === 'modified')
},
ascOrdered () {
return (this.req.order === 'asc')
},
@@ -97,7 +120,14 @@ export default {
return 'arrow_downward'
},
sizeIcon () {
if (!this.nameSorted && this.ascOrdered) {
if (this.sizeSorted && this.ascOrdered) {
return 'arrow_downward'
}
return 'arrow_upward'
},
modifiedIcon () {
if (this.modifiedSorted && this.ascOrdered) {
return 'arrow_downward'
}
@@ -275,10 +305,14 @@ export default {
if (this.nameIcon === 'arrow_upward') {
order = 'asc'
}
} else {
} else if (sort === 'size') {
if (this.sizeIcon === 'arrow_upward') {
order = 'asc'
}
} else if (sort === 'modified') {
if (this.modifiedIcon === 'arrow_upward') {
order = 'asc'
}
}
let path = this.$store.state.baseURL

View File

@@ -1,5 +1,7 @@
<template>
<div class="item"
role="button"
tabindex="0"
draggable="true"
@dragstart="dragStart"
@dragover="dragOver"
@@ -7,6 +9,7 @@
@click="click"
@dblclick="open"
@touchstart="touchstart"
:aria-label="name"
:aria-selected="isSelected">
<div>
<i class="material-icons">{{ icon }}</i>

View File

@@ -1,7 +1,7 @@
<template>
<div id="previewer">
<div class="bar">
<button @click="back" class="action" aria-label="Close Preview" id="close">
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
<i class="material-icons">close</i>
</button>
@@ -11,8 +11,12 @@
<info-button></info-button>
</div>
<button class="action" @click="prev" v-show="hasPrevious"><i class="material-icons">chevron_left</i></button>
<button class="action" @click="next" v-show="hasNext"><i class="material-icons">chevron_right</i></button>
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
<i class="material-icons">chevron_left</i>
</button>
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<i class="material-icons">chevron_right</i>
</button>
<div class="preview">
<img v-if="req.type == 'image'" :src="raw()">
@@ -24,7 +28,7 @@
</video>
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object>
<a v-else-if="req.type == 'blob'" :href="download()">
<h2 class="message">Download <i class="material-icons">file_download</i></h2>
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
</a>
<pre v-else >{{ req.content }}</pre>
</div>
@@ -35,10 +39,10 @@
import { mapState } from 'vuex'
import url from '@/utils/url'
import api from '@/utils/api'
import InfoButton from './buttons/Info'
import DeleteButton from './buttons/Delete'
import RenameButton from './buttons/Rename'
import DownloadButton from './buttons/Download'
import InfoButton from '@/components/buttons/Info'
import DeleteButton from '@/components/buttons/Delete'
import RenameButton from '@/components/buttons/Rename'
import DownloadButton from '@/components/buttons/Download'
export default {
name: 'preview',

View File

@@ -1,13 +1,19 @@
<template>
<div class="prompt">
<h3>Copy</h3>
<p>Choose the place to copy your files:</p>
<h3>{{ $t('prompts.copy') }}</h3>
<p>{{ $t('prompts.copyMessage') }}</p>
<file-list @update:selected="val => dest = val"></file-list>
<div>
<button class="ok" @click="copy">Copy</button>
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
<button class="ok"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>

View File

@@ -1,11 +1,16 @@
<template>
<div class="prompt">
<h3>Delete files</h3>
<p v-show="req.kind !== 'listing'">Are you sure you want to delete this file/folder?</p>
<p v-show="req.kind === 'listing'">Are you sure you want to delete {{ selectedCount }} file(s)?</p>
<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" autofocus>Delete</button>
<button @click="closeHovers" class="cancel">Cancel</button>
<button @click="submit"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>

View File

@@ -1,7 +1,8 @@
<template>
<div class="prompt" id="download">
<h3>Download files</h3>
<p>Choose the format you want to download.</p>
<h3>{{ $t('prompts.download') }}</h3>
<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>

View File

@@ -1,11 +1,17 @@
<template>
<div class="prompt error">
<i class="material-icons">error_outline</i>
<h3>Something went wrong</h3>
<h3>{{ $t('prompts.error') }}</h3>
<pre>{{ $store.state.showMessage }}</pre>
<div>
<button @click="close" autofocus>Close</button>
<button @click="reportIssue" class="cancel">Report Issue</button>
<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>

View File

@@ -4,12 +4,15 @@
<li @click="select"
@touchstart="touchstart"
@dblclick="next"
role="button"
tabindex="0"
:aria-label="item.name"
:aria-selected="selected == item.url"
:key="item.name" v-for="item in items"
:data-url="item.url">{{ item.name }}</li>
</ul>
<p>Currently navigating on: <code>{{ nav }}</code>.</p>
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
</div>
</template>

View File

@@ -1,26 +1,25 @@
<template>
<div class="prompt help">
<h3>Help</h3>
<h3>{{ $t('help.help') }}</h3>
<ul>
<li><strong>F1</strong> - this information</li>
<li><strong>F2</strong> - rename file</li>
<li><strong>DEL</strong> - delete selected items</li>
<li><strong>ESC</strong> - clear selection and/or close the prompt</li>
<li><strong>CTRL + S</strong> - save a file or download the directory where you are</li>
<li><strong>CTRL + Click</strong> - select multiple files or directories</li>
<li><strong>Double click</strong> - open a file or directory</li>
<li><strong>Click</strong> - select file or directory</li>
</ul>
<p>Not available yet</p>
<ul>
<li><strong>Alt + Click</strong> - select a group of files</li>
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
</ul>
<div>
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
<button type="submit"
@click="$store.commit('closeHovers')"
class="ok"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div>
</div>
</template>

View File

@@ -1,27 +1,31 @@
<template>
<div class="prompt">
<h3>File Information</h3>
<h3>{{ $t('prompts.fileInfo') }}</h3>
<p v-show="selected.length > 1">{{ selected.length }} files selected.</p>
<p v-show="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
<p v-show="selected.length < 2"><strong>Display Name:</strong> {{ name() }}</p>
<p><strong>Size:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
<p v-show="selected.length < 2"><strong>Last Modified:</strong> {{ humanTime() }}</p>
<p v-show="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>
<section v-show="dir() && selected.length === 0">
<p><strong>Number of files:</strong> {{ req.numFiles }}</p>
<p><strong>Number of directories:</strong> {{ req.numDirs }}</p>
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
</section>
<section v-show="!dir()">
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">show</a></code></p>
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">show</a></code></p>
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">show</a></code></p>
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">show</a></code></p>
<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>
<div>
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
<button type="submit"
@click="$store.commit('closeHovers')"
class="ok"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div>
</div>
</template>

View File

@@ -1,13 +1,19 @@
<template>
<div class="prompt">
<h3>Move</h3>
<p>Choose new house for your file(s)/folder(s):</p>
<h3>{{ $t('prompts.move') }}</h3>
<p>{{ $t('prompts.moveMessage') }}</p>
<file-list @update:selected="val => dest = val"></file-list>
<div>
<button class="ok" @click="move">Move</button>
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
<button class="ok"
@click="move"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>

View File

@@ -1,11 +1,17 @@
<template>
<div class="prompt">
<h3>New directory</h3>
<p>Write the name of the new directory.</p>
<h3>{{ $t('prompts.newDir') }}</h3>
<p>{{ $t('prompts.newDirMessage') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button class="ok" @click="submit">Create</button>
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
<button class="ok"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
@click="submit">{{ $t('buttons.create') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>

View File

@@ -1,11 +1,17 @@
<template>
<div class="prompt">
<h3>New file</h3>
<p>Write the name of the new file.</p>
<h3>{{ $t('prompts.newFile') }}</h3>
<p>{{ $t('prompts.newFileMessage') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button class="ok" @click="submit">Create</button>
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
<button class="ok"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>

View File

@@ -26,8 +26,14 @@
:name="input.name"
:placeholder="input.placeholder">
<div>
<input type="submit" class="ok" :value="prompt.ok">
<button class="cancel" @click.prevent="$store.commit('closeHovers')">Cancel</button>
<input type="submit" class="ok"
:aria-label="prompt.ok"
:title="prompt.ok"
:value="prompt.ok">
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</form>
</template>

View File

@@ -1,11 +1,18 @@
<template>
<div class="prompt">
<h3>Rename</h3>
<p>Insert a new name for <code>{{ oldName() }}</code>:</p>
<h3>{{ $t('prompts.rename') }}</h3>
<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">Rename</button>
<button @click="cancel" class="cancel">Cancel</button>
<button @click="submit"
type="submit"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>

View File

@@ -3,7 +3,10 @@
<i class="material-icons">done</i>
<h3>{{ $store.state.showMessage }}</h3>
<div>
<button @click="close" autofocus>OK</button>
<button @click="close"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')"
autofocus>{{ $t('buttons.ok') }}</button>
</div>
</div>
</template>

View File

@@ -133,5 +133,15 @@ main {
}
#breadcrumbs a {
color: inherit
color: inherit;
transition: .1s ease-in;
border-radius: .125em;
}
#breadcrumbs a:hover {
background-color: rgba(0,0,0, 0.05);
}
#breadcrumbs span a {
padding: .2em;
}

View File

@@ -35,7 +35,7 @@
width: 1em
}
.dashboard > *:first-child {
.dashboard > h1:first-of-type {
margin-top: 0;
}
@@ -48,6 +48,7 @@ form.dashboard > p:last-child {
margin-bottom: 0;
}
.dashboard select,
.dashboard textarea,
.dashboard input[type="text"],
.dashboard input[type="password"] {
@@ -60,12 +61,18 @@ form.dashboard > p:last-child {
width: 100%;
}
.dashboard #locale,
.dashboard #username,
.dashboard #password,
.dashboard #scope {
max-width: 18em;
}
.dashboard #locale {
border: 1px solid #dddddd;
margin-top: .5em;
}
.dashboard textarea:focus,
.dashboard textarea:hover,
.dashboard input[type="text"]:focus,
@@ -118,3 +125,27 @@ p code {
font-size: .8em;
line-height: 1.5;
}
.dashboard #nav {
list-style: none;
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);
}
.dashboard #nav li {
width: 100%;
}
.dashboard #nav li:last-child {
text-align: right
}
.dashboard #nav i {
font-size: 1em;
vertical-align: middle;
}

View File

@@ -177,11 +177,24 @@ header .search-button {
#search.active #result i {
color: #ccc;
}
#search.active #result > p > i {
text-align: center;
margin: 0 auto;
display: table;
}
#search.active #result ul li a {
display: flex;
align-items: center;
padding: .3em 0;
}
#search.active #result ul li a i {
margin-right: .3em;
}
#search::-webkit-input-placeholder {
color: rgba(255, 255, 255, .5);
}

68
assets/src/css/login.css Normal file
View File

@@ -0,0 +1,68 @@
#login {
background: #fff;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#login img {
width: 4em;
height: 4em;
margin: 0 auto;
display: block;
}
#login h1 {
text-align: center;
font-size: 2.5em;
margin: .4em 0 .67em;
}
#login form {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 16em;
width: 90%;
}
#login input {
width: 100%;
width: 100%;
margin: .5em 0 0;
}
#login .wrong {
background: #F44336;
color: #fff;
padding: .5em;
text-align: center;
animation: .2s opac forwards;
}
@keyframes opac {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
#login input[type="text"],
#login input[type="password"] {
padding: .5em 1em;
border: 1px solid #e9e9e9;
transition: .2s ease border;
color: #333;
}
#login input[type="text"]:focus,
#login input[type="password"]:focus,
#login input[type="text"]:hover,
#login input[type="password"]:hover {
border-color: #9f9f9f;
}

View File

@@ -6,6 +6,7 @@
@import "./listing.css";
@import "./editor.css";
@import "./dashboard.css";
@import "./login.css";
/* * * * * * * * * * * * * * * *
* ACTION *

167
assets/src/i18n/en.yaml Normal file
View File

@@ -0,0 +1,167 @@
buttons:
cancel: Cancel
close: Close
copy: Copy
copyFile: Copy file
create: Create
delete: Delete
download: Download
info: Info
more: More
move: Move
moveFile: Move file
new: New
next: Next
ok: OK
previous: Previous
rename: Rename
reportIssue: Report Issue
save: Save
search: Search
select: Select
selectMultiple: Select multiple
switchView: Swicth view
toggleSidebar: Toggle sidebar
update: Update
upload: Upload
errors:
forbidden: You're not welcome here.
internal: Something really went wrong.
notFound: This location can't be reached.
files:
folders: Folders
files: Files
body: Body
clear: Clear
closePreview: Close preview
home: Home
lastModified: Last modified
loading: Loading...
lonely: It feels lonely here...
metadata: Metadata
multipleSelectionEnabled: Multiple selection enabled
name: Name
size: Size
sortByName: Sort by name
sortBySize: Sort by size
sortByLastModified: Sort by last modified
help:
click: select file or directory
ctrl:
click: select multiple files or directories
f: opens search
s: save a file or download the directory where you are
del: delete selected items
doubleClick: open a file or directory
esc: clear selection and/or close the prompt
f1: this information
f2: rename file
help: Help
login:
password: Password
submit: Login
username: Username
wrongCredentials: Wrong credentials
prompts:
copy: Copy
copyMessage: 'Choose the place to copy your files:'
currentlyNavigating: 'Currently navigating on:'
deleteMessageMultiple: Are you sure you want to delete {count} file(s)?
deleteMessageSingle: Are you sure you want to delete this file/folder?
deleteTitle: Delete files
displayName: 'Display Name:'
download: Download files
downloadMessage: Choose the format you want to download.
error: Something went wrong
fileInfo: File information
filesSelected: "{count} files selected."
lastModified: Last Modified
move: Move
moveMessage: 'Choose new house for your file(s)/folder(s):'
newDir: New directory
newDirMessage: Write the name of the new directory.
newFile: New file
newFileMessage: Write the name of the new file.
numberDirs: Number of directories
numberFiles: Number of files
rename: Rename
renameMessage: Insert a new name for
show: Show
size: Size
settings:
admin: Admin
administrator: Administrator
allowCommands: Execute commands
allowEdit: Edit, rename and delete files or directories.
allowNew: Create new files and directories
avoidChanges: "(leave blank to avoid changes)"
changePassword: Change Password
commands: Commands
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
of the file.
commandsUpdated: Commands updated!
customStylesheet: Custom Stylesheet
examples: Examples
globalSettings: Global Settings
language: Language
newPassword: Your new password
newPasswordConfirm: Confirm your new password
newUser: New User
password: Password
passwordUpdated: Password updated!
permissions: Permissions
permissionsHelp: >
You can set the user to be an administrator or choose the permissions
individually. If you select "Administrator", all of the other options will be
automatically checked. The management of users remains a privilege of an administrator.
pluginsUpdated: Plugins settings updated!
profileSettings: Profile Settings
ruleExample1: >
'prevents the access to any dot file (such as .git, .gitignore) in
every folder.'
ruleExample2: blocks the access to the file named Caddyfile on the root of the scope.
rules: Rules
rulesHelp1: >
'Here you can define a set of allow and disallow rules for this specific
user. The blocked files won''t show up in the listings and they won''t be accessible
to the user. We support regex and paths relative to the user''s scope.'
rulesHelp2: >
Each rule goes in one different line and must start with the keyword
{0} or {1}. Then you should write {2} if you are using a regular expression and
then the expression or the path.
scope: Scope
settingsUpdated: Settings updated!
user: User
userCommands: Commands
userCommandsHelp:
'A space separated list with the available commands for this user.
Example:'
userCreated: User created!
userDeleted: User deleted!
userManagement: User Management
username: Username
users: Users
userUpdated: User updated!
sidebar:
help: Help
logout: Logout
myFiles: My files
newFile: New file
newFolder: New folder
servedWith: Served with
settings: Settings
search:
writeToSearch: Write here to search
searchOrCommand: Search or execute a command...
searchOrSupportedCommand: 'Search or use one of your supported commands:'
search: Search...
type: Type and press enter to search.
pressToSearch: Press enter to search.
pressToExecute: Press enter to execute.
languages:
en: English
pt: Portuguese
zhCN: Chinese (Simplified)

19
assets/src/i18n/index.js Normal file
View File

@@ -0,0 +1,19 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import en from './en.yaml'
import pt from './pt.yaml'
import zhCN from './zh-cn.yaml'
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: 'en',
fallbackLocale: 'en',
messages: {
'en': en,
'pt': pt,
'zh-cn': zhCN
}
})
export default i18n

168
assets/src/i18n/pt.yaml Normal file
View File

@@ -0,0 +1,168 @@
buttons:
cancel: Cancelar
close: Fechar
copy: Copiar
copyFile: Copiar ficheiro
create: Criar
delete: Eliminar
download: Descarregar
info: Info
more: Mais
move: Mover
moveFile: Mover ficheiro
new: Novo
next: Próximo
ok: OK
previous: Anterior
rename: Renomear
reportIssue: Reportar Erro
save: Guardar
search: Pesquisar
select: Selecionar
selectMultiple: Selecionar múltiplos
switchView: Alterar modo de visão
toggleSidebar: Alternar barra lateral
update: Atualizar
upload: Enviar
errors:
forbidden: Tu não és bem-vindo aqui.
internal: Algo correu bastante mal.
notFound: Não conseguimos chegar a esta localização.
files:
folders: Pastas
files: Ficheiros
body: Corpo
clear: Limpar
closePreview: Fechar pré-visualização
home: Início
lastModified: Última modificação
loading: A carregar...
lonely: Sinto-me sozinho...
metadata: Metadados
multipleSelectionEnabled: Seleção múltipla ativada
name: Nome
size: Tamanho
sortByName: Ordenar pelo nome
sortBySize: Ordenar pelo tamanho
sortByLastModified: Ordenar pela última modificação
help:
click: selecionar pasta ou ficheiro
ctrl:
click: selecionar várias pastas e ficheiros
f: pesquisar
s: guardar um ficheiro ou descarregar a pasta em que estás a navegar
del: eliminar os ficheiros selecionados
doubleClick: abrir pasta ou ficheiro
esc: limpar seleção e/ou fechar menu
f1: esta informação
f2: renomear ficheiro
help: Ajuda
login:
password: Palavra-passe
submit: Login
username: Nome de utilizador
wrongCredentials: Dados errados
prompts:
copy: Copiar
copyMessage: 'Escolhe um lugar para copiar os ficheiros:'
currentlyNavigating: 'A navegar em:'
deleteMessageMultiple: Deseja eliminar {count} ficheiro(s)?
deleteMessageSingle: Deseja eliminar esta pasta/ficheiro?
deleteTitle: Eliminar ficheiros
displayName: 'Nome:'
download: Descarregar ficheiros
downloadMessage: Escolha o formato do ficheiro.
error: Algo correu mal
fileInfo: Informação do ficheiro
filesSelected: "{count} ficheiros selecionados."
lastModified: Última Modificação
move: Mover
moveMessage: 'Escolha uma nova casa para os seus ficheiros:'
newDir: Nova pasta
newDirMessage: Escreva o nome da nova pasta.
newFile: Novo ficheiro
newFileMessage: Escreva o nome do novo ficheiro.
numberDirs: Número de pastas
numberFiles: Número de ficheiros
rename: Renomear
renameMessage: Insira um novo nome para
show: Mostrar
size: Tamanho
settings:
admin: Admin
administrator: Administrador
allowCommands: Executar comandos
allowEdit: Editar, renomear e eliminar ficheiros ou pastas
allowNew: Criar novos ficheiros e pastas
avoidChanges: "(deixe em branco para manter)"
changePassword: Alterar Password
commands: Comandos
commandsHelp: >
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.
commandsUpdated: Comandos atualizados!
customStylesheet: Estilos Personalizados
examples: Exemplos
globalSettings: Configurações Globais
language: Linguagem
newPassword: Nova palavra-passe
newPasswordConfirm: Confirme a nova palavra-passe
newUser: Novo Utilizador
password: Palavra-passe
passwordUpdated: Palavra-passe atualizada!
permissions: Permissões
permissionsHelp: >
Pode definir o utilizador como administrador ou escolher as permissões manualmente.
Se selecionar a opção "Administrador", todas as outras opções serão automaticamente
selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores.
pluginsUpdated: Plugins atualizados!
profileSettings: Configurações do Utilizador
ruleExample1: >
previne o acesso a qualquer "dotfile" (como .git, .gitignore) em qualquer pasta
ruleExample2: bloqueia o acesso ao ficheiro chamado Caddyfile.
rules: Regras
rulesHelp1: >
Aqui pode definir um conjunto de regras para permitir ou bloquear o acesso
do utilizador a determinados ficheiros ou pastas. Os ficheiros bloqueados não
irão aparecer durante a navegação. Suportamos expressões regulares e os caminhos
dos ficheiros devem ser relativos à base do utilizador.
rulesHelp2: >
Cada regra deve ser colocada numa linha diferente e deve começar com as
palavras {0} (permite) ou {1} (bloqueia). Deve escrever, logo de seguida, {2},
caso queira utilizar uma expressão regular. Depois, escreva o caminho do ficheiro/pasta
ou a expressão regular.
scope: Base
settingsUpdated: Configurações atualizadas!
user: Utilizador
userCommands: Comandos
userCommandsHelp:
'Uma lista, separada com espaços, de comandos disponíveis para este
utilizados. Exemplo:'
userCreated: Utilizador criado!
userDeleted: Utilizador eliminado!
userManagement: Gestão de Utilizadores
username: Nome de utilizador
users: Utilizadores
userUpdated: Utilizador atualizado!
sidebar:
help: Ajuda
logout: Sair
myFiles: Ficheiros
newFile: Novo ficheiro
newFolder: Nova pasta
servedWith: Servido com
settings: Configurações
search:
writeToSearch: Escreva aqui para pesquisar
searchOrCommand: Pesquise ou execute um comando...
searchOrSupportedCommand: 'Pesquise ou utilize um dos seus comandos:'
search: Pesquise...
type: Escreva e prima enter para pesquisar.
pressToSearch: Prima enter para pesquisar.
pressToExecute: Prima enter para executar.
languages:
en: Inglês
pt: Português
zhCN: Chinês (Simplificado)

157
assets/src/i18n/zh-cn.yaml Normal file
View File

@@ -0,0 +1,157 @@
buttons:
cancel: 取消
close: 关闭
copy: 复制
copyFile: 复制文件
create: 创建
delete: 删除
download: 下载
info: 信息
more: 更多
move: 移动
moveFile: 移动文件
new:
next: 下一步
ok: 确定
previous: 以前
rename: 重命名
reportIssue: 报告问题
save: 保存
search: 搜索
select: 选择
selectMultiple: 选择多个
switchView: 切换显示方式
toggleSidebar: 切换侧边栏
update: 更新
upload: 上传
errors:
forbidden: 你被禁止访问.
internal: 内部出现麻烦了.
notFound: 找不到文件.
files:
folders: 文件夹
files: 文件
body: Body
clear: 清理
closePreview: 关闭预览
home: 主页
lastModified: 最后修改
loading: 加载中...
lonely: 这里没有任何文件...
metadata: 元数据
multipleSelectionEnabled: 启用多选模式(现在可以选择多个文件/文件夹)
name: 名称
size: 大小
help:
click: 选择文件或目录
ctrl:
click: 选择多个文件或目录
f: 打开搜索框
s: 保存文件或下载文件夹
del: 删除 所选文件/文件夹
doubleClick: 打开文件或目录
esc: 清除 当前所有选择 或 关闭提示信息
f1: 显示 当前帮助信息
f2: 重命名 文件/文件夹
help: 帮助
login:
password: 密码
submit: 登录
username: 用户名
wrongCredentials: 账号或密码错误
prompts:
copy: 复制
copyMessage: '请选择欲复制至的目录:'
currentlyNavigating: '目前正在浏览:'
deleteMessageMultiple: 你确定要删除这 {count} 个文件吗?
deleteMessageSingle: 你确定要删除这个文件/文件夹吗?
deleteTitle: 删除文件
displayName: '名称:'
download: 下载文件
downloadMessage: 请选择要下载的压缩格式.
error: 出了一点问题...
fileInfo: 文件信息
filesSelected: '选择 {count} 个文件.'
lastModified: 最后修改
move: 移动
moveMessage: '请选择欲移动至的目录:'
newDir: 新建目录
newDirMessage: 请输入新建目录的名称.
newFile: 新建文件
newFileMessage: 请输入新建文件的名称.
numberDirs: 目录数
numberFiles: 文件数
rename: 重命名
renameMessage: '请输入新名称, 旧名称是:'
show: 揭示
size: 大小
settings:
admin: 管理员
administrator: 管理员
allowCommands: 执行命令(Linux 代码)
allowEdit: 编辑、重命名或删除文件/目录.
allowNew: 创建新文件和目录.
avoidChanges: '(留空以避免更改)'
changePassword: 更改密码
commands: 命令(linux 代码)
commandsHelp: >
'Here you can set commands that are executed in the named events.
每行一条命令. If the event is related to files, such as before and after saving,
the environment variable "file" will be available with the path of the file.'
commandsUpdated: 命令更新!
customStylesheet: 自定义样式表
examples: 例子
globalSettings: 全局设置
language: 语言
newPassword: 您的新密码
newPasswordConfirm: 重输一遍新密码
newUser: 新建用户
password: 密码
passwordUpdated: 密码更新!
permissions: 权限
permissionsHelp: >
'您可以将该用户设置为管理员 或单独选择各项权限. 如果选择 "管理员(Administrator)" ,
将自动检查所有其他选项, 并且该用户可以管理其他用户.'
pluginsUpdated: 插件设置更新!
profileSettings: 配置文件设置
ruleExample1: >
'阻止用户访问每个文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore).'
ruleExample2: 阻止用户访问其目录范围内任何名为 Caddyfile 的文件/文件夹.
rules: 规则
rulesHelp1: >
'这里您可以为特定用户制定一组允许或不允许的规则,
阻止的文件将不会显示到列表中, 用户将无法访问, 支持相对于用户的范围.'
rulesHelp2: >
每行一条规则, 必须以关键词 {0} 或 {1} 开头. 如果使用正则表达式,
然后使用表达式或路径, 则需要在第二列单词加入 {2} .
scope: 目录范围
settingsUpdated: 设置更新!
user: 用户
userCommands: 用户命令(Linux 代码)
userCommandsHelp: '一个以空格分割的列表, 用于指定该用户可以执行的命令(Linux 代码), 例如:'
userCreated: 用户创建!
userDeleted: 用户删除!
userManagement: 用户管理
username: 用户名
users: 用户
userUpdated: 用户更新!
sidebar:
help: 帮助
logout: 注销
myFiles: 我的文件
newFile: 新建文件
newFolder: 新建文件夹
servedWith: 服务提供
settings: 设置
search:
writeToSearch: 请输入要搜索的内容
searchOrCommand: 搜索或者执行命令(Linux 代码)...
searchOrSupportedCommand: '搜索或使用您支持使用的命令(一次只能执行一个命令):'
search: 搜索...
type: 键入并按 Enter 键(回车)进行搜索.
pressToSearch: 按 Enter 键(回车)进行搜索.
pressToExecute: 按 Enter 键(回车)执行.
languages:
en: English
pt: Portuguese
zhCN: Chinese (Simplified)

View File

@@ -2,6 +2,7 @@ import Vue from 'vue'
import App from './App'
import store from './store'
import router from './router'
import i18n from './i18n'
Vue.config.productionTip = true
@@ -10,6 +11,7 @@ new Vue({
el: '#app',
store,
router,
i18n,
template: '<App/>',
components: { App }
})

View File

@@ -1,15 +1,15 @@
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login'
import Main from '@/components/Main'
import Files from '@/components/Files'
import Users from '@/components/Users'
import User from '@/components/User'
import GlobalSettings from '@/components/GlobalSettings'
import ProfileSettings from '@/components/ProfileSettings'
import error403 from '@/components/errors/403'
import error404 from '@/components/errors/404'
import error500 from '@/components/errors/500'
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 Error403 from '@/views/errors/403'
import Error404 from '@/views/errors/404'
import Error500 from '@/views/errors/500'
import auth from '@/utils/auth.js'
import store from '@/store'
@@ -25,24 +25,18 @@ const router = new Router({
component: Login,
beforeEnter: function (to, from, next) {
auth.loggedIn()
.then(() => {
next({ path: '/files' })
})
.catch(() => {
document.title = 'Login'
next()
})
}
},
{
path: '/',
redirect: {
path: '/files/'
.then(() => {
next({ path: '/files' })
})
.catch(() => {
document.title = 'Login'
next()
})
}
},
{
path: '/*',
component: Main,
component: Layout,
meta: {
requiresAuth: true
},
@@ -75,17 +69,17 @@ const router = new Router({
{
path: '/403',
name: 'Forbidden',
component: error403
component: Error403
},
{
path: '/404',
name: 'Not Found',
component: error404
component: Error404
},
{
path: '/500',
name: 'Internal Server Error',
component: error500
component: Error500
},
{
path: '/users',
@@ -95,12 +89,6 @@ const router = new Router({
requiresAdmin: true
}
},
{
path: '/users/',
redirect: {
path: '/users'
}
},
{
path: '/users/*',
name: 'User',
@@ -109,6 +97,12 @@ const router = new Router({
requiresAdmin: true
}
},
{
path: '/files',
redirect: {
path: '/files/'
}
},
{
path: '/*',
redirect: {

View File

@@ -1,3 +1,5 @@
import i18n from '@/i18n'
const mutations = {
closeHovers: state => {
state.show = null
@@ -22,8 +24,10 @@ const mutations = {
},
setLoading: (state, value) => { state.loading = value },
setReload: (state, value) => { state.reload = value },
setUser: (state, value) => (state.user = value),
setUserCSS: (state, value) => (state.user.css = value),
setUser: (state, value) => {
i18n.locale = value.locale
state.user = value
},
setJWT: (state, value) => (state.jwt = value),
multiple: (state, value) => (state.multiple = value),
addSelected: (state, value) => (state.selected.push(value)),

View File

@@ -2,7 +2,7 @@ import store from '@/store'
const ssl = (window.location.protocol === 'https:')
function removePrefix (url) {
export function removePrefix (url) {
if (url.startsWith('/files')) {
return url.slice(6)
}
@@ -10,7 +10,7 @@ function removePrefix (url) {
return url
}
function fetch (url) {
export function fetch (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -24,10 +24,7 @@ function fetch (url) {
resolve(JSON.parse(request.responseText))
break
default:
reject({
message: request.responseText,
status: request.status
})
reject(new Error(request.status))
break
}
}
@@ -36,7 +33,7 @@ function fetch (url) {
})
}
function rm (url) {
export function rm (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -57,7 +54,7 @@ function rm (url) {
})
}
function post (url, content = '') {
export function post (url, content = '') {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -78,7 +75,7 @@ function post (url, content = '') {
})
}
function put (url, content = '') {
export function put (url, content = '') {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -132,15 +129,15 @@ function moveCopy (items, copy = false) {
return Promise.all(promises)
}
function move (items) {
export function move (items) {
return moveCopy(items)
}
function copy (items) {
export function copy (items) {
return moveCopy(items, true)
}
function checksum (url, algo) {
export function checksum (url, algo) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -160,10 +157,10 @@ function checksum (url, algo) {
})
}
function command (url, command, onmessage, onclose) {
export function command (url, command, onmessage, onclose) {
let protocol = (ssl ? 'wss:' : 'ws:')
url = removePrefix(url)
url = `${protocol}//${window.location.hostname}${store.state.baseURL}/api/command${url}`
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/command${url}`
let conn = new window.WebSocket(url)
conn.onopen = () => conn.send(command)
@@ -171,10 +168,10 @@ function command (url, command, onmessage, onclose) {
conn.onclose = onclose
}
function search (url, search, onmessage, onclose) {
export function search (url, search, onmessage, onclose) {
let protocol = (ssl ? 'wss:' : 'ws:')
url = removePrefix(url)
url = `${protocol}//${window.location.hostname}${store.state.baseURL}/api/search${url}`
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/search${url}`
let conn = new window.WebSocket(url)
conn.onopen = () => conn.send(search)
@@ -182,7 +179,7 @@ function search (url, search, onmessage, onclose) {
conn.onclose = onclose
}
function download (format, ...files) {
export function download (format, ...files) {
let url = `${store.state.baseURL}/api/download`
if (files.length === 1) {
@@ -206,7 +203,59 @@ function download (format, ...files) {
window.open(url)
}
function getUsers () {
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}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve(JSON.parse(request.responseText))
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
export function updateSettings (param, which) {
return new Promise((resolve, reject) => {
let data = {
what: 'settings',
which: which,
data: {}
}
data.data[which] = param
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => { reject(error) }
request.send(JSON.stringify(data))
})
}
// USERS
export function getUsers () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/`, true)
@@ -227,7 +276,7 @@ function getUsers () {
})
}
function getUser (id) {
export function getUser (id) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
@@ -248,7 +297,7 @@ function getUser (id) {
})
}
function newUser (user) {
export function newUser (user) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/users/`, true)
@@ -265,11 +314,15 @@ function newUser (user) {
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify(user))
request.send(JSON.stringify({
what: 'user',
which: 'new',
data: user
}))
})
}
function updateUser (user) {
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)
@@ -286,11 +339,15 @@ function updateUser (user) {
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify(user))
request.send(JSON.stringify({
what: 'user',
which: (typeof which === 'string') ? which : 'all',
data: user
}))
})
}
function deleteUser (id) {
export function deleteUser (id) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
@@ -311,133 +368,8 @@ function deleteUser (id) {
})
}
function updatePassword (password) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/users/change-password`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify({ 'password': password }))
})
}
function updateCSS (css) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/users/change-css`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify({ 'css': css }))
})
}
function getCommands () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/commands/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve(JSON.parse(request.responseText))
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
function updateCommands (commands) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/commands/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify(commands))
})
}
function getPlugins () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/plugins/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve(JSON.parse(request.responseText))
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
function updatePlugins (data) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/plugins/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify(data))
})
}
export default {
removePrefix,
delete: rm,
fetch,
checksum,
@@ -448,16 +380,13 @@ export default {
command,
search,
download,
getUser,
// other things
getSettings,
updateSettings,
// User things
newUser,
updateUser,
getUser,
getUsers,
updatePassword,
updateCSS,
getCommands,
updateCommands,
removePrefix,
getPlugins,
updatePlugins,
updateUser,
deleteUser
}

View File

@@ -23,10 +23,10 @@ function loggedIn () {
parseToken(request.responseText)
resolve()
} else {
reject()
reject(new Error(request.responseText))
}
}
request.onerror = () => reject()
request.onerror = () => reject(new Error('Could not finish the request'))
request.send()
})
}
@@ -45,7 +45,7 @@ function login (user, password) {
reject(request.responseText)
}
}
request.onerror = () => reject()
request.onerror = () => reject(new Error('Could not finish the request'))
request.send(JSON.stringify(data))
})
}

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div id="breadcrumbs">
<router-link to="/files/">
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
<i class="material-icons">home</i>
</router-link>
@@ -11,8 +11,8 @@
</span>
</div>
<div v-if="error">
<not-found v-if="error === 404"></not-found>
<forbidden v-else-if="error === 403"></forbidden>
<not-found v-if="error.message === '404'"></not-found>
<forbidden v-else-if="error.message === '403'"></forbidden>
<internal-error v-else></internal-error>
</div>
<editor v-else-if="isEditor"></editor>
@@ -20,7 +20,7 @@
<preview v-else-if="isPreview"></preview>
<div v-else>
<h2 class="message">
<span>Loading...</span>
<span>{{ $t('files.loading') }}</span>
</h2>
</div>
</div>
@@ -30,9 +30,9 @@
import Forbidden from './errors/403'
import NotFound from './errors/404'
import InternalError from './errors/500'
import Preview from './Preview'
import Listing from './Listing'
import Editor from './Editor'
import Preview from '@/components/files/Preview'
import Listing from '@/components/files/Listing'
import Editor from '@/components/files/Editor'
import api from '@/utils/api'
import { mapGetters, mapState, mapMutations } from 'vuex'
@@ -116,20 +116,14 @@ export default {
},
mounted () {
window.addEventListener('keydown', this.keyEvent)
window.addEventListener('scroll', event => {
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
let top = 112 - window.scrollY
if (top < 64) {
top = 64
}
document.querySelector('#listing.list .item.header').style.top = top + 'px'
})
window.addEventListener('scroll', this.scroll)
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
window.removeEventListener('scroll', this.scroll)
},
destroyed () {
this.$store.commit('updateRequest', {})
},
methods: {
...mapMutations([ 'setLoading' ]),
@@ -149,25 +143,19 @@ export default {
if (url[0] !== '/') url = '/' + url
api.fetch(url)
.then((req) => {
if (!url.endsWith('/') && req.url.endsWith('/')) {
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
}
.then((req) => {
if (!url.endsWith('/') && req.url.endsWith('/')) {
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
}
this.$store.commit('updateRequest', req)
document.title = req.name
this.setLoading(false)
})
.catch(error => {
this.setLoading(false)
if (typeof error === 'object') {
this.error = error.status
return
}
this.error = error
})
this.$store.commit('updateRequest', req)
document.title = req.name
this.setLoading(false)
})
.catch(error => {
this.setLoading(false)
this.error = error
})
},
keyEvent (event) {
// Esc!
@@ -217,11 +205,21 @@ export default {
if (this.req.kind !== 'editor') {
document.getElementById('download-button').click()
return
}
}
}
},
scroll (event) {
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
let top = 112 - window.scrollY
if (top < 64) {
top = 64
}
document.querySelector('#listing.list .item.header').style.top = top + 'px'
},
openSidebar () {
this.$store.commit('showHover', 'sidebar')
},

View File

@@ -1,17 +1,25 @@
<template>
<div class="dashboard">
<h1>Global Settings</h1>
<ul>
<li><router-link to="/settings/profile">Go to Profile Settings</router-link></li>
<li><router-link to="/users">Go to User Management</router-link></li>
<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="savePlugin" v-if="plugins.length > 0">
<template v-for="plugin in plugins">
<h2>{{ capitalize(plugin.name) }}</h2>
<p v-for="field in plugin.fields" :key="field.name">
<p v-for="field in plugin.fields" :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">
@@ -23,11 +31,9 @@
</form>
<form @submit="saveCommands">
<h2>Commands</h2>
<h2>{{ $t('settings.commands') }}</h2>
<p class="small">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
<code>file</code> will be available with the path of the file.</p>
<p class="small">{{ $t('settings.commandsHelp') }}</p>
<template v-for="command in commands">
<h3>{{ capitalize(command.name) }}</h3>
@@ -42,7 +48,7 @@
<script>
import { mapState, mapMutations } from 'vuex'
import api from '@/utils/api'
import { getSettings, updateSettings } from '@/utils/api'
export default {
name: 'settings',
@@ -56,63 +62,20 @@ export default {
...mapState([ 'user' ])
},
created () {
api.getCommands()
.then(commands => {
for (let key in commands) {
getSettings()
.then(settings => {
for (let key in settings.plugins) {
this.plugins.push(this.parsePlugin(key, settings.plugins[key]))
}
for (let key in settings.commands) {
this.commands.push({
name: key,
value: commands[key].join('\n')
value: settings.commands[key].join('\n')
})
}
})
.catch(error => { this.showError(error) })
api.getPlugins()
.then(plugins => {
console.log(plugins)
let plugin = {}
for (let key in plugins) {
plugin.name = key
plugin.fields = []
for (let field in plugins[key]) {
let value = plugins[key][field]
if (Array.isArray(value)) {
plugin.fields.push({
name: field,
type: 'text',
original: 'array',
value: value.join(' ')
})
continue
}
switch (typeof value) {
case 'boolean':
plugin.fields.push({
name: field,
type: 'checkbox',
original: 'boolean',
value: value
})
break
default:
plugin.fields.push({
name: field,
type: 'text',
original: 'text',
value: value
})
}
}
this.plugins.push(plugin)
}
})
.catch(error => { this.showError(error) })
},
methods: {
...mapMutations([ 'showSuccess', 'showError' ]),
@@ -141,8 +104,8 @@ export default {
commands[command.name] = value
}
api.updateCommands(commands)
.then(() => { this.showSuccess('Commands updated!') })
updateSettings(commands, 'commands')
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
.catch(error => { this.showError(error) })
},
savePlugin (event) {
@@ -153,7 +116,7 @@ export default {
let p = {}
for (let field of plugin.fields) {
p[field.name] = field.value
p[field.variable] = field.value
if (field.original === 'array') {
let val = field.value.split(' ')
@@ -161,18 +124,53 @@ export default {
val.shift()
}
p[field.name] = val
p[field.variable] = val
}
}
plugins[plugin.name] = p
}
console.log(plugins)
api.updatePlugins(plugins)
.then(() => { this.showSuccess('Plugins settings updated!') })
updateSettings(plugins, 'plugins')
.then(() => { this.showSuccess(this.$t('settings.pluginsUpdated')) })
.catch(error => { this.showError(error) })
},
parsePlugin (name, plugin) {
let obj = {
name: name,
fields: []
}
for (let option of plugin) {
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(' ')
obj.fields.push(field)
continue
}
switch (typeof value) {
case 'boolean':
field.type = 'checkbox'
field.original = 'boolean'
break
}
obj.fields.push(field)
}
return obj
}
}
}

View File

@@ -10,13 +10,13 @@
</template>
<script>
import Search from './Search'
import Sidebar from './Sidebar'
import Prompts from './prompts/Prompts'
import SiteHeader from './Header'
import Search from '@/components/Search'
import Sidebar from '@/components/Sidebar'
import Prompts from '@/components/prompts/Prompts'
import SiteHeader from '@/components/Header'
export default {
name: 'main',
name: 'layout',
components: {
Search,
Sidebar,

View File

@@ -0,0 +1,42 @@
<template>
<div id="login">
<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')">
<input type="submit" :value="$t('login.submit')">
</form>
</div>
</template>
<script>
import auth from '@/utils/auth'
export default {
name: 'login',
data: function () {
return {
wrong: false,
username: '',
password: ''
}
},
methods: {
submit: function (event) {
event.preventDefault()
event.stopPropagation()
let redirect = this.$route.query.redirect
if (redirect === '' || redirect === undefined || redirect === null) {
redirect = '/files/'
}
auth.login(this.username, this.password)
.then(() => { this.$router.push({ path: redirect }) })
.catch(() => { this.wrong = true })
}
}
}
</script>

View File

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

@@ -1,58 +1,56 @@
<template>
<div>
<form @submit="save" class="dashboard">
<h1 v-if="id === 0">New User</h1>
<h1 v-else>User {{ username }}</h1>
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
<p><label for="username">Username</label><input type="text" v-model="username" id="username"></p>
<p><label for="password">Password</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
<p><label for="scope">Scope</label><input type="text" v-model="filesystem" id="scope"></p>
<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>Permissions</h2>
<h2>{{ $t('settings.permissions') }}</h2>
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
<p class="small">You can set the user to be an administrator or choose the permissions individually.
If you select "Administrator", all of the other options will be automatically checked.
The management of users remains a privilege of an administrator.</p>
<p><input type="checkbox" v-model="admin"> Administrator</p>
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> Create new files and directories</p>
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> Edit, rename and delete files or directories.</p>
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> Execute commands</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-for="(value, key) in permissions" :key="key">
<input type="checkbox" :disabled="admin" v-model="permissions[key]"> {{ capitalize(key) }}
</p>
<h3>Commands</h3>
<p class="small">A space separated list with the available commands for this user. Example: <i>git svn hg</i>.</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>Rules</h2>
<h2>{{ $t('settings.rules') }}</h2>
<p class="small">Here you can define a set of allow and disallow rules for this specific user. The blocked files won't
show up in the listings and they won't be accessible to the user. We support regex and paths relative to
the user's scope.</p>
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
<p class="small">Each rule goes in one different line and must start with the keyword <code>allow</code> or <code>disallow</code>.
Then you should write <code>regex</code> if you are using a regular expression and then the expression or the path.</p>
<i18n path="settings.rulesHelp2" tag="p" class="small">
<code>allow</code><code>disallow</code><code>regex</code>
</i18n>
<p class="small"><strong>Examples</strong></p>
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
<ul class="small">
<li><code>disallow regex \\/\\..+</code> - prevents the access to any dot file (such as .git, .gitignore) in every folder.</li>
<li><code>disallow /Caddyfile</code> - blocks the access to the file named <i>Caddyfile</i> on the root of the scope</li>
<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>Custom Stylesheet</h2>
<h2>{{ $t('settings.customStylesheet') }}</h2>
<textarea name="css"></textarea>
<p>
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete">Delete</button>
<input type="submit" value="Save">
<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>
@@ -60,8 +58,13 @@
<h3>Delete User</h3>
<p>Are you sure you want to delete this user?</p>
<div>
<button @click="deleteUser" autofocus>Delete</button>
<button @click="closeHovers" class="cancel">Cancel</button>
<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>
@@ -69,10 +72,12 @@
<script>
import { mapMutations } from 'vuex'
import api from '@/utils/api'
import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
import Languages from '@/components/Languages'
export default {
name: 'user',
components: { Languages },
data: () => {
return {
id: 0,
@@ -85,6 +90,7 @@ export default {
username: '',
filesystem: '',
rules: '',
locale: '',
css: '',
commands: ''
}
@@ -92,7 +98,7 @@ export default {
computed: {
passwordPlaceholder () {
if (this.$route.path === '/users/new') return ''
return '(leave blank to avoid changes)'
return this.$t('settings.avoidChanges')
}
},
created () {
@@ -119,7 +125,7 @@ export default {
user = 'base'
}
api.getUser(user).then(user => {
getUser(user).then(user => {
this.id = user.ID
this.admin = user.admin
this.allowCommands = user.allowCommands
@@ -130,6 +136,7 @@ export default {
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) {
@@ -173,6 +180,7 @@ export default {
this.username = ''
this.filesystem = ''
this.rules = ''
this.locale = ''
this.css = ''
this.commands = ''
},
@@ -182,9 +190,9 @@ export default {
deleteUser (event) {
event.preventDefault()
api.deleteUser(this.id).then(location => {
deleteUser(this.id).then(location => {
this.$router.push({ path: '/users' })
this.$store.commit('showSuccess', 'User deleted!')
this.$store.commit('showSuccess', this.$t('settings.userDeleted'))
}).catch(e => {
this.$store.commit('showError', e)
})
@@ -194,9 +202,9 @@ export default {
let user = this.parseForm()
if (this.$route.path === '/users/new') {
api.newUser(user).then(location => {
newUser(user).then(location => {
this.$router.push({ path: location })
this.$store.commit('showSuccess', 'User created!')
this.$store.commit('showSuccess', this.$t('settings.userCreated'))
}).catch(e => {
this.$store.commit('showError', e)
})
@@ -204,8 +212,12 @@ export default {
return
}
api.updateUser(user).then(location => {
this.$store.commit('showSuccess', 'User updated!')
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)
})
@@ -222,6 +234,7 @@ export default {
allowEdit: this.allowEdit,
permissions: this.permissions,
css: this.css,
locale: this.locale,
commands: this.commands.split(' '),
rules: []
}
@@ -269,7 +282,3 @@ export default {
}
}
</script>
<style>
</style>

View File

@@ -1,12 +1,12 @@
<template>
<div class="dashboard">
<h1>Users <router-link to="/users/new"><button>New</button></router-link></h1>
<h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1>
<table>
<tr>
<th>Username</th>
<th>Admin</th>
<th>Scope</th>
<th>{{ $t('settings.username') }}</th>
<th>{{ $t('settings.admin') }}</th>
<th>{{ $t('settings.scope') }}</th>
<th></th>
</tr>

View File

@@ -2,7 +2,7 @@
<div>
<h2 class="message">
<i class="material-icons">error</i>
<span>You're not welcome here.</span>
<span>{{ $t('errors.forbidden') }}</span>
</h2>
</div>
</template>

View File

@@ -2,7 +2,7 @@
<div>
<h2 class="message">
<i class="material-icons">gps_off</i>
<span>This location can't be reached.</span>
<span>{{ $t('errors.notFound') }}</span>
</h2>
</div>
</template>

View File

@@ -2,7 +2,7 @@
<div>
<h2 class="message">
<i class="material-icons">error_outline</i>
<span>Something really went wrong.</span>
<span>{{ $t('errors.internal') }}</span>
</h2>
</div>
</template>

23
auth.go
View File

@@ -15,6 +15,11 @@ import (
// authHandler proccesses the authentication for the user.
func authHandler(c *RequestContext, 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
if r.Body == nil {
@@ -27,7 +32,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
}
// Checks if the user exists.
u, ok := c.FM.Users[cred.Username]
u, ok := c.Users[cred.Username]
if !ok {
return http.StatusForbidden, nil
}
@@ -56,6 +61,7 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// claims is the JWT claims.
type claims struct {
User
NoAuth bool `json:"noAuth"`
jwt.StandardClaims
}
@@ -70,6 +76,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
// Builds the claims.
claims := claims{
u,
c.NoAuth,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
Issuer: "File Manager",
@@ -78,14 +85,15 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
// Creates the token and signs it.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
string, err := token.SignedString(c.FM.key)
signed, err := token.SignedString(c.key)
if err != nil {
return http.StatusInternalServerError, err
}
// Writes the token.
w.Write([]byte(string))
w.Header().Set("Content-Type", "cty")
w.Write([]byte(signed))
return 0, nil
}
@@ -112,8 +120,13 @@ 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) {
if c.NoAuth {
c.User = c.DefaultUser
return true, c.User
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
return c.FM.key, nil
return c.key, nil
}
var claims claims
token, err := request.ParseFromRequestWithClaims(r,
@@ -126,7 +139,7 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
return false, nil
}
u, ok := c.FM.Users[claims.User.Username]
u, ok := c.Users[claims.User.Username]
if !ok {
return false, nil
}

View File

@@ -1,6 +1,13 @@
#!/bin/bash
# Install rice tool if not present
if ! [ -x "$(command -v rice)" ]; then
go get github.com/GeertJohan/go.rice/rice
fi
# Clean the dist folder and build the assets
rm -rf assets/dist
npm run build
rice embed-go
cd ./caddy/hugo
# Embed the assets using rice
rice embed-go

View File

@@ -10,10 +10,11 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
. "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/dir"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
@@ -73,6 +74,7 @@ func parse(c *caddy.Controller) ([]*config, error) {
baseURL := "/"
baseScope := "."
database := ""
noAuth := false
// Get the baseURL and baseScope
args := c.RemainingArgs()
@@ -93,6 +95,17 @@ func parse(c *caddy.Controller) ([]*config, error) {
}
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
}
}
}
@@ -136,13 +149,14 @@ func parse(c *caddy.Controller) ([]*config, error) {
Regexp: &Regexp{Raw: "\\/\\..+"},
}},
CSS: "",
FileSystem: dir.Dir(baseScope),
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, "/"))

View File

@@ -1,79 +0,0 @@
# hugo - a caddy plugin
[![community](https://img.shields.io/badge/community-forum-ff69b4.svg?style=flat-square)](https://caddy.community)
hugo fills the gap between Hugo and the browser. [Hugo][6] is an easy and fast static website generator. This plugin fills the gap between Hugo and the end-user, providing you a web interface to manage the whole website.
Using this plugin, you won't need to have your own computer to edit posts, neither regenerate your static website, because you can do all of that just through your browser. It is an implementation of [hacdias/filemanager][1] library.
**Requirements:** you need to have the hugo executable in your PATH. You can download it from its [official page][6].
## Get Started
To start using this plugin you just need to go to the [download Caddy page][3] and choose `http.hugo` in the directives section. For further information on how Caddy works refer to [its documentation][4].
The default credentials are `admin` for both the user and the password. It is highy recommended to change them after logging in for the first time and to use HTTPS. You can create more users and define their own permissions using the web interface.
## Syntax
```
hugo [directory] [admin] {
database path
}
```
+ `directory` is the path, relative or absolute to the directory of your Hugo files. Defaults to `./`.
+ `admin` is the URL path where you will access the admin interface. Defaults to `/admin`.
+ `path` is the database path where the settings will be stored. By default, the settings will be stored on [`.caddy`][5] folder.
## Database
By default the database will be stored on [`.caddy`][5] directory, in a sub-directory called `hugo`. Each file name is an hash of the combination of the host and the base URL.
If you don't set a database path, you will receive a warning like this:
> [WARNING] A database is going to be created for your File Manager instace at ~/.caddy/hugo/xxx.db. It is highly recommended that you set the 'database' option to 'xxx.db'
Why? If you don't set a database path and you change the host or the base URL, your settings will be reseted. So it is *highly* recommended to set this option.
When you set a relative path, such as `xxxxxxxxxx.db`, it will always be relative to `.caddy/hugo` directory. Although, you may also use an absolute path if you wish to store the database in other place.
## Examples
Manage the current working directory's Hugo website at `/admin` and display the ```public``` folder to the user.
```
root public
hugo {
database myinstance.db
}
```
Manage the Hugo website located at `/var/www/mysite` at `/admin` and display the ```public``` folder to the user.
```
root /var/www/mysite/public
hugo /var/www/mysite {
database myinstance.db
}
```
Manage the Hugo website located at `/var/www/mysite` at `/private` and display the ```public``` folder to the user.
```
root /var/www/mysite/public
hugo /var/www/mysite /private {
database myinstance.db
}
```
## Known Issues
If you are having troubles **handling large files** you might need to check out the [`timeouts`][2] plugin, which can be used to change the default HTTP Timeouts.
[1]:https://github.com/hacdias/filemanager
[2]:https://caddyserver.com/docs/timeouts
[3]:https://caddyserver.com/download
[4]:https://caddyserver.com/docs
[5]:https://caddyserver.com/docs/automatic-https#dot-caddy
[6]:http://gohugo.io

View File

@@ -1,194 +1,182 @@
package hugo
import (
"log"
"crypto/md5"
"encoding/hex"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
rice "github.com/GeertJohan/go.rice"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/variables"
"github.com/robfig/cron"
"github.com/hacdias/filemanager/plugins"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
type hugo struct {
// Website root
Root string `description:"The relative or absolute path to the place where your website is located."`
// Public folder
Public string `description:"The relative or absolute path to the public folder."`
// Hugo executable path
Exe string `description:"The absolute path to the Hugo executable or the command to execute."`
// Hugo arguments
Args []string `description:"The arguments to run when running Hugo"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `description:"Indicates if the public folder should be cleaned before publishing the website."`
// setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error {
configs, err := parse(c)
if err != nil {
return err
}
// TODO: admin interface to cgange options
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next}
})
return nil
}
func (h hugo) BeforeAPI(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// If we are using the 'magic url' for the settings, we should redirect the
// request for the acutual path.
if r.URL.Path == "/settings/" || r.URL.Path == "/settings" {
var frontmatter string
var err error
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
var (
configs []*filemanager.FileManager
)
if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil {
frontmatter = "yaml"
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 _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil {
frontmatter = "json"
if len(args) > 1 {
admin = args[1]
}
if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil {
frontmatter = "toml"
}
for c.NextBlock() {
switch c.Val() {
case "database":
if !c.NextArg() {
return nil, c.ArgErr()
}
r.URL.Path = "/config." + frontmatter
return 0, nil
}
database = c.Val()
case "no_auth":
if !c.NextArg() {
noAuth = true
continue
}
// From here on, we only care about 'hugo' router so we can bypass
// the others.
if c.Router != "hugo" {
return 0, nil
}
// If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method.
if r.Method != http.MethodPost {
return http.StatusMethodNotAllowed, nil
}
// If we are creating a file built from an archetype.
if r.Header.Get("Archetype") != "" {
if !c.User.AllowNew {
return http.StatusForbidden, nil
}
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
filename = strings.TrimPrefix(filename, "/")
archetype := r.Header.Get("archetype")
ext := filepath.Ext(filename)
// If the request isn't for a markdown file, we can't
// handle it.
if ext != ".markdown" && ext != ".md" {
return http.StatusBadRequest, errUnsupportedFileType
}
// Tries to create a new file based on this archetype.
args := []string{"new", filename, "--kind", archetype}
if err := Run(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err
}
// Writes the location of the new file to the Header.
w.Header().Set("Location", "/files/content/"+filename)
return http.StatusCreated, nil
}
// If we are trying to regenerate the website.
if r.Header.Get("Regenerate") == "true" {
if !c.User.Permissions["allowPublish"] {
return http.StatusForbidden, nil
}
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
filename = strings.TrimPrefix(filename, "/")
// Before save command handler.
if err := c.FM.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
// We only run undraft command if it is a file.
if !strings.HasSuffix(filename, "/") {
args := []string{"undraft", filename}
if err := Run(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err
var err error
noAuth, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
}
}
// Regenerates the file
h.run(false)
caddyConf := httpserver.GetConfig(c)
// Executed the before publish command.
if err := c.FM.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
path := filepath.Join(caddy.AssetsPath(), "hugo")
err := os.MkdirAll(path, 0700)
if err != nil {
return nil, err
}
return http.StatusOK, nil
}
if r.Header.Get("Schedule") != "" {
if !c.User.Permissions["allowPublish"] {
return http.StatusForbidden, nil
// 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)
}
return h.schedule(c, w, r)
// 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{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
Permissions: map[string]bool{},
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 := &plugins.Hugo{
Root: directory,
Public: filepath.Join(directory, "public"),
Args: []string{},
CleanPublic: true,
}
// Try to find the Hugo executable path.
if err = hugo.Find(); err != nil {
return nil, err
}
// Attaches Hugo plugin to this file manager instance.
err = m.ActivatePlugin("hugo", 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 http.StatusNotFound, nil
return configs, nil
}
func (h hugo) AfterAPI(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
func (h hugo) JavaScript() string {
return rice.MustFindBox("./").MustString("hugo.js")
}
// run runs Hugo with the define arguments.
func (h hugo) run(force bool) {
// If the CleanPublic option is enabled, clean it.
if h.CleanPublic {
os.RemoveAll(h.Public)
}
// Prevent running if watching is enabled
if b, pos := variables.StringInSlice("--watch", h.Args); b && !force {
if len(h.Args) > pos && h.Args[pos+1] != "false" {
return
// 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
}
if len(h.Args) == pos+1 {
return
}
p.Configs[i].ServeHTTP(w, r)
return 0, nil
}
if err := Run(h.Exe, h.Args, h.Root); err != nil {
log.Println(err)
}
return p.Next.ServeHTTP(w, r)
}
// schedule schedules a post to be published later.
func (h hugo) schedule(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
t, err := time.Parse("2006-01-02T15:04", r.Header.Get("Schedule"))
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
path = filepath.Clean(path)
if err != nil {
return http.StatusInternalServerError, err
}
scheduler := cron.New()
scheduler.AddFunc(t.Format("05 04 15 02 01 *"), func() {
args := []string{"undraft", path}
if err := Run(h.Exe, args, h.Root); err != nil {
log.Printf(err.Error())
return
}
h.run(false)
func init() {
caddy.RegisterPlugin("hugo", caddy.Plugin{
ServerType: "http",
Action: setup,
})
scheduler.Start()
return http.StatusOK, nil
}
type plugin struct {
Next httpserver.Handler
Configs []*filemanager.FileManager
}

File diff suppressed because one or more lines are too long

View File

@@ -1,188 +0,0 @@
package hugo
import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/dir"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
var (
errHugoNotFound = errors.New("It seems that tou don't have 'hugo' on your PATH")
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
)
// 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 := ""
// 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()
}
}
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{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
Permissions: map[string]bool{},
Commands: []string{"git", "svn", "hg"},
Rules: []*filemanager.Rule{{
Regex: true,
Allow: false,
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
}},
CSS: "",
FileSystem: dir.Dir(directory),
})
if err != nil {
return nil, err
}
// Initialize the default settings for Hugo.
hugo := &hugo{
Root: directory,
Public: filepath.Join(directory, "public"),
Args: []string{},
CleanPublic: true,
}
// Try to find the Hugo executable path.
if hugo.Exe, err = exec.LookPath("hugo"); err != nil {
return nil, errHugoNotFound
}
err = m.RegisterPlugin("hugo", hugo)
if err != nil {
return nil, err
}
err = m.RegisterEventType("before_publish")
if err != nil {
return nil, err
}
err = m.RegisterEventType("after_publish")
if err != nil {
return nil, err
}
err = m.RegisterPermission("allowPublish", true)
if err != nil {
return nil, err
}
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",
Action: setup,
})
}
type plugin struct {
Next httpserver.Handler
Configs []*filemanager.FileManager
}

View File

@@ -1,31 +1,24 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"strconv"
"os"
"path/filepath"
"strings"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/dir"
)
lumberjack "gopkg.in/natefinch/lumberjack.v2"
// confFile contains the configuration file for this File Manager instance.
// If the user chooses to use a configuration file, the flags will be ignored.
type confFile struct {
Database string `json:"database"`
Scope string `json:"scope"`
Address string `json:"address"`
Commands []string `json:"commands"`
Port int `json:"port"`
AllowCommands bool `json:"allowCommands"`
AllowEdit bool `json:"allowEdit"`
AllowNew bool `json:"allowNew"`
}
"github.com/hacdias/filemanager/plugins"
"github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
)
var (
addr string
@@ -33,77 +26,177 @@ var (
database string
scope string
commands string
port string
logfile string
plugin string
locale string
port int
noAuth bool
allowCommands bool
allowEdit bool
allowNew bool
showVer bool
version = "master"
)
func init() {
flag.StringVar(&config, "config", "", "JSON configuration file")
flag.StringVar(&port, "port", "0", "HTTP Port (default is random)")
flag.StringVar(&addr, "address", "", "Address to listen to (default is all of them)")
flag.StringVar(&database, "database", "./filemanager.db", "Database path")
flag.StringVar(&scope, "scope", ".", "Default scope for new users")
flag.StringVar(&commands, "commands", "git svn hg", "Space separated commands available for new users")
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option")
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option")
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option")
flag.StringVarP(&config, "config", "c", "", "Configuration file")
flag.IntVarP(&port, "port", "p", 0, "HTTP Port (default is random)")
flag.StringVarP(&addr, "address", "a", "", "Address to listen to (default is all of them)")
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.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
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(&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(&plugin, "plugin", "", "Plugin you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
}
func setupViper() {
viper.SetDefault("Address", "")
viper.SetDefault("Port", "0")
viper.SetDefault("Database", "./filemanager.db")
viper.SetDefault("Scope", ".")
viper.SetDefault("Logger", "stdout")
viper.SetDefault("Commands", []string{"git", "svn", "hg"})
viper.SetDefault("AllowCommmands", true)
viper.SetDefault("AllowEdit", true)
viper.SetDefault("AllowNew", true)
viper.SetDefault("Plugin", "")
viper.SetDefault("Locale", "en")
viper.SetDefault("NoAuth", false)
viper.BindPFlag("Port", flag.Lookup("port"))
viper.BindPFlag("Address", flag.Lookup("address"))
viper.BindPFlag("Database", flag.Lookup("database"))
viper.BindPFlag("Scope", flag.Lookup("scope"))
viper.BindPFlag("Logger", flag.Lookup("log"))
viper.BindPFlag("Commands", flag.Lookup("commands"))
viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
viper.BindPFlag("Locale", flag.Lookup("locale"))
viper.BindPFlag("Plugin", flag.Lookup("plugin"))
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
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)
}
os.Exit(0)
}
func main() {
setupViper()
flag.Parse()
if config != "" {
loadConfig()
if showVer {
printVersion()
}
fm, err := filemanager.New(database, filemanager.User{
AllowCommands: allowCommands,
AllowEdit: allowEdit,
AllowNew: allowNew,
Commands: strings.Split(strings.TrimSpace(commands), " "),
// Add a configuration file if set.
if config != "" {
ext := filepath.Ext(config)
dir := filepath.Dir(config)
config = strings.TrimSuffix(config, ext)
if dir != "" {
viper.AddConfigPath(dir)
config = strings.TrimPrefix(config, dir)
}
viper.SetConfigName(config)
}
// Read configuration from a file if exists.
err := viper.ReadInConfig()
if err != nil {
if _, ok := err.(viper.ConfigParseError); ok {
panic(err)
}
}
// Set up process log before anything bad happens.
switch viper.GetString("Logger") {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "":
log.SetOutput(ioutil.Discard)
default:
log.SetOutput(&lumberjack.Logger{
Filename: logfile,
MaxSize: 100,
MaxAge: 14,
MaxBackups: 10,
})
}
// 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"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
FileSystem: dir.Dir(scope),
FileSystem: fileutils.Dir(viper.GetString("Scope")),
})
if err != nil {
panic(err)
if viper.GetBool("NoAuth") {
fm.NoAuth = true
}
fm.SetBaseURL("/")
fm.SetPrefixURL("/")
listener, err := net.Listen("tcp", addr+":"+port)
if err != nil {
panic(err)
log.Fatal(err)
}
if viper.GetString("Plugin") == "hugo" {
// Initialize the default settings for Hugo.
hugo := &plugins.Hugo{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{},
CleanPublic: true,
}
// Try to find the Hugo executable path.
if err = hugo.Find(); err != nil {
log.Fatal(err)
}
if err = fm.ActivatePlugin("hugo", hugo); err != nil {
log.Fatal(err)
}
}
// Builds the address and a listener.
laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
listener, err := net.Listen("tcp", laddr)
if err != nil {
log.Fatal(err)
}
// Tell the user the port in which is listening.
fmt.Println("Listening on", listener.Addr().String())
// Starts the server.
if err := http.Serve(listener, fm); err != nil {
panic(err)
log.Fatal(err)
}
}
func loadConfig() {
file, err := ioutil.ReadFile(config)
if err != nil {
panic(err)
}
var conf *confFile
err = json.Unmarshal(file, &conf)
if err != nil {
panic(err)
}
database = conf.Database
scope = conf.Scope
addr = conf.Address
commands = strings.Join(conf.Commands, " ")
port = strconv.Itoa(conf.Port)
allowNew = conf.AllowNew
allowEdit = conf.AllowEdit
allowCommands = conf.AllowCommands
}

View File

@@ -1,230 +0,0 @@
// Package dir implements a FileSystem interface using the native
// file system restricted to a specific directory tree. Originally from
// https://github.com/golang/net/blob/master/webdav/file.go#L68
package dir
import (
"errors"
"io"
"os"
"path"
"path/filepath"
"strings"
)
// A Dir uses the native file system restricted to a specific directory tree.
//
// While the FileSystem.OpenFile method takes '/'-separated paths, a Dir's
// string value is a filename on the native file system, not a URL, so it is
// separated by filepath.Separator, which isn't necessarily '/'.
//
// An empty Dir is treated as ".".
type Dir string
func (d Dir) resolve(name string) string {
// This implementation is based on Dir.Open's code in the standard net/http package.
if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
strings.Contains(name, "\x00") {
return ""
}
dir := string(d)
if dir == "" {
dir = "."
}
return filepath.Join(dir, filepath.FromSlash(SlashClean(name)))
}
// Mkdir implements os.Mkdir in this directory context.
func (d Dir) Mkdir(name string, perm os.FileMode) error {
if name = d.resolve(name); name == "" {
return os.ErrNotExist
}
return os.Mkdir(name, perm)
}
// OpenFile implements os.OpenFile in this directory context.
func (d Dir) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
if name = d.resolve(name); name == "" {
return nil, os.ErrNotExist
}
f, err := os.OpenFile(name, flag, perm)
if err != nil {
return nil, err
}
return f, nil
}
// RemoveAll implements os.RemoveAll in this directory context.
func (d Dir) RemoveAll(name string) error {
if name = d.resolve(name); name == "" {
return os.ErrNotExist
}
if name == filepath.Clean(string(d)) {
// Prohibit removing the virtual root directory.
return os.ErrInvalid
}
return os.RemoveAll(name)
}
// Rename implements os.Rename in this directory context.
func (d Dir) Rename(oldName, newName string) error {
if oldName = d.resolve(oldName); oldName == "" {
return os.ErrNotExist
}
if newName = d.resolve(newName); newName == "" {
return os.ErrNotExist
}
if root := filepath.Clean(string(d)); root == oldName || root == newName {
// Prohibit renaming from or to the virtual root directory.
return os.ErrInvalid
}
return os.Rename(oldName, newName)
}
// Stat implements os.Stat in this directory context.
func (d Dir) Stat(name string) (os.FileInfo, error) {
if name = d.resolve(name); name == "" {
return nil, os.ErrNotExist
}
return os.Stat(name)
}
// Copy copies a file or directory from src to dst. If it is
// a directory, all of the files and sub-directories will be copied.
func (d Dir) Copy(src, dst string) error {
if src = d.resolve(src); src == "" {
return os.ErrNotExist
}
if dst = d.resolve(dst); dst == "" {
return os.ErrNotExist
}
if root := filepath.Clean(string(d)); root == src || root == dst {
// Prohibit copying from or to the virtual root directory.
return os.ErrInvalid
}
if dst == src {
return os.ErrInvalid
}
info, err := os.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return CopyDir(src, dst)
}
return CopyFile(src, dst)
}
// SlashClean is equivalent to but slightly more efficient than
// path.Clean("/" + name).
func SlashClean(name string) string {
if name == "" || name[0] != '/' {
name = "/" + name
}
return path.Clean(name)
}
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(source string, dest string) error {
// Open the source file.
src, err := os.Open(source)
if err != nil {
return err
}
defer src.Close()
// Makes the directory needed to create the dst
// file.
err = os.MkdirAll(filepath.Dir(dest), 0666)
if err != nil {
return err
}
// Create the destination file.
dst, err := os.Create(dest)
if err != nil {
return err
}
defer dst.Close()
// Copy the contents of the file.
_, err = io.Copy(dst, src)
if err != nil {
return err
}
// Copy the mode if the user can't
// open the file.
info, err := os.Stat(source)
if err != nil {
err = os.Chmod(dest, info.Mode())
if err != nil {
return err
}
}
return nil
}
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(source string, dest string) error {
// Get properties of source.
srcinfo, err := os.Stat(source)
if err != nil {
return err
}
// Create the destination directory.
err = os.MkdirAll(dest, srcinfo.Mode())
if err != nil {
return err
}
dir, _ := os.Open(source)
obs, err := dir.Readdir(-1)
var errs []error
for _, obj := range obs {
fsource := source + "/" + obj.Name()
fdest := dest + "/" + obj.Name()
if obj.IsDir() {
// Create sub-directories, recursively.
err = CopyDir(fsource, fdest)
if err != nil {
errs = append(errs, err)
}
} else {
// Perform the file copy.
err = CopyFile(fsource, fdest)
if err != nil {
errs = append(errs, err)
}
}
}
var errString string
for _, err := range errs {
errString += err.Error() + "\n"
}
if errString != "" {
return errors.New(errString)
}
return nil
}

56
doc.go
View File

@@ -1,56 +0,0 @@
/*
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

View File

@@ -9,7 +9,7 @@ import (
"path/filepath"
"strings"
"github.com/hacdias/filemanager/dir"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
)
@@ -20,14 +20,14 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// If the file isn't a directory, serve it using http.ServeFile. We display it
// inline if it is requested.
if !c.FI.IsDir {
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.FI.Name)
w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name)
}
http.ServeFile(w, r, c.FI.Path)
http.ServeFile(w, r, c.File.Path)
return 0, nil
}
@@ -45,11 +45,11 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Clean the slashes.
name = dir.SlashClean(name)
files = append(files, filepath.Join(c.FI.Path, name))
name = fileutils.SlashClean(name)
files = append(files, filepath.Join(c.File.Path, name))
}
} else {
files = append(files, c.FI.Path)
files = append(files, c.File.Path)
}
// If the format is true, just set it to "zip".
@@ -93,7 +93,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Defines the file name.
name := c.FI.Name
name := c.File.Name
if name == "." || name == "" {
name = "download"
}

21
file.go
View File

@@ -110,7 +110,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
func (i *file) getListing(c *RequestContext, r *http.Request) error {
// Gets the directory information using the Virtual File System of
// the user configuration.
f, err := c.User.FileSystem.OpenFile(c.FI.VirtualPath, os.O_RDONLY, 0)
f, err := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0)
if err != nil {
return err
}
@@ -328,6 +328,8 @@ func (l listing) ApplySort() {
sort.Sort(sort.Reverse(byName(l)))
case "size":
sort.Sort(sort.Reverse(bySize(l)))
case "modified":
sort.Sort(sort.Reverse(byModified(l)))
default:
// If not one of the above, do nothing
return
@@ -338,6 +340,8 @@ func (l listing) ApplySort() {
sort.Sort(byName(l))
case "size":
sort.Sort(bySize(l))
case "modified":
sort.Sort(byModified(l))
default:
sort.Sort(byName(l))
return
@@ -348,6 +352,7 @@ func (l listing) ApplySort() {
// Implement sorting for listing
type byName listing
type bySize listing
type byModified listing
// By Name
func (l byName) Len() int {
@@ -392,6 +397,20 @@ func (l bySize) Less(i, j int) bool {
return iSize < jSize
}
// By Modified
func (l byModified) Len() int {
return len(l.Items)
}
func (l byModified) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
func (l byModified) Less(i, j int) bool {
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
return iModified.Sub(jModified) < 0
}
var textExtensions = [...]string{
".md", ".markdown", ".mdown", ".mmark",
".asciidoc", ".adoc", ".ad",

View File

@@ -1,3 +1,56 @@
// 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 (
@@ -6,20 +59,26 @@ import (
"net/http"
"os"
"os/exec"
"reflect"
"regexp"
"strings"
rice "github.com/GeertJohan/go.rice"
"github.com/asdine/storm"
"github.com/hacdias/filemanager/dir"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
)
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")
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")
plugins = map[string]Plugin{}
)
// FileManager is a file manager instance. It should be creating using the
@@ -44,6 +103,10 @@ type FileManager struct {
// edited directly. Use SetBaseURL.
BaseURL string
// NoAuth disables the authentication. When the authentication is disabled,
// there will only exist one user, called "admin".
NoAuth bool
// The Default User needed to build the New User page.
DefaultUser *User
@@ -53,8 +116,8 @@ type FileManager struct {
// A map of events to a slice of commands.
Commands map[string][]string
// The plugins that have been plugged in.
Plugins map[string]Plugin
// The options of the plugins that have been plugged into this instance.
Plugins map[string]interface{}
}
// Command is a command function.
@@ -76,7 +139,7 @@ type User struct {
Admin bool `json:"admin"`
// FileSystem is the virtual file system the user has access.
FileSystem dir.Dir `json:"filesystem"`
FileSystem fileutils.Dir `json:"filesystem"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
@@ -84,6 +147,9 @@ type User struct {
// 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
@@ -115,15 +181,32 @@ type Regexp struct {
regexp *regexp.Regexp
}
// Plugin is a File Manager plugin.
type Plugin interface {
// The JavaScript that will be injected into the main page.
JavaScript() string
type Plugin struct {
JavaScript string
CommandEvents []string
Permissions []Permission
Handler PluginHandler
Options interface{}
}
type Permission struct {
Name string
Value bool
}
type PluginHandler interface {
// If the Plugin returns (0, nil), the executation of File Manager will procced as usual.
// Otherwise it will stop.
BeforeAPI(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
AfterAPI(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Before(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
After(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
}
func RegisterPlugin(name string, plugin Plugin) {
if _, ok := plugins[name]; ok {
panic(name + " plugin is already registred")
}
plugins[name] = plugin
}
// DefaultUser is used on New, when no 'base' user is provided.
@@ -136,7 +219,8 @@ var DefaultUser = User{
Rules: []*Rule{},
CSS: "",
Admin: true,
FileSystem: dir.Dir("."),
Locale: "en",
FileSystem: fileutils.Dir("."),
}
// New creates a new File Manager instance. If 'database' file already
@@ -148,8 +232,8 @@ func New(database string, base User) (*FileManager, error) {
// map and Assets box.
m := &FileManager{
Users: map[string]*User{},
Plugins: map[string]interface{}{},
assets: rice.MustFindBox("./assets/dist"),
Plugins: map[string]Plugin{},
}
// Tries to open a database on the location provided. This
@@ -265,41 +349,63 @@ func (m *FileManager) SetBaseURL(url string) {
m.BaseURL = strings.TrimSuffix(url, "/")
}
// RegisterPlugin registers a plugin to a File Manager instance and
// ActivatePlugin activates a plugin to a File Manager instance and
// loads its options from the database.
func (m *FileManager) RegisterPlugin(name string, plugin Plugin) error {
func (m *FileManager) ActivatePlugin(name string, options interface{}) error {
if reflect.TypeOf(options).Kind() != reflect.Ptr {
return errors.New("options should be a pointer to interface, not interface")
}
var plugin Plugin
if p, ok := plugins[name]; !ok {
plugin = p
return errors.New(name + " plugin is not registred")
}
if _, ok := m.Plugins[name]; ok {
return errors.New("Plugin already registred")
return errors.New(name + " plugin is already activated")
}
err := m.db.Get("plugins", name, &plugin)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("plugins", name, plugin)
err = m.db.Set("plugin", name, plugin)
}
if err != nil {
return err
}
m.Plugins[name] = plugin
// Register the command event hooks.
for _, evt := range plugin.CommandEvents {
if _, ok := m.Commands[evt]; ok {
continue
}
m.Commands[evt] = []string{}
}
err = m.db.Set("config", "commands", m.Commands)
if err != nil {
return err
}
// Register the user permissions.
for _, perm := range plugin.Permissions {
err = m.registerPermission(perm.Name, perm.Value)
if err != nil {
return err
}
}
m.Plugins[name] = options
return nil
}
// RegisterEventType registers a new event type which can be triggered using Runner
// function.
func (m *FileManager) RegisterEventType(name string) error {
if _, ok := m.Commands[name]; ok {
return nil
}
m.Commands[name] = []string{}
return m.db.Set("config", "commands", m.Commands)
}
// RegisterPermission registers a new user permission and adds it to every
// registerPermission registers a new user permission and adds it to every
// user with it default's 'value'. If the user is an admin, it will
// be true.
func (m *FileManager) RegisterPermission(name string, value bool) error {
func (m *FileManager) registerPermission(name string, value bool) error {
if _, ok := m.DefaultUser.Permissions[name]; ok {
return nil
}
@@ -334,20 +440,25 @@ func (m *FileManager) RegisterPermission(name string, value bool) error {
// Compatible with http.Handler.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := serveHTTP(&RequestContext{
FM: m,
User: nil,
FI: nil,
FileManager: m,
User: nil,
File: nil,
}, w, r)
if code != 0 {
if code >= 400 {
w.WriteHeader(code)
if err != nil {
w.Write([]byte(err.Error()))
} else {
w.Write([]byte(http.StatusText(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()))
}
}
// Allowed checks if the user has permission to access a directory/file.

View File

@@ -6,7 +6,7 @@ import (
"path/filepath"
"testing"
"github.com/hacdias/filemanager/dir"
"github.com/hacdias/fileutils"
)
type test struct {
@@ -28,13 +28,13 @@ func newTest(t *testing.T) *test {
scope := filepath.Join(temp, "scope")
database := filepath.Join(temp, "database.db")
err = dir.CopyDir("./testdata", scope)
err = fileutils.CopyDir("./testdata", scope)
if err != nil {
t.Fatalf("Error copying the test data: %v", err)
}
user := DefaultUser
user.FileSystem = dir.Dir(scope)
user.FileSystem = fileutils.Dir(scope)
fm, err := New(database, user)

51
http.go
View File

@@ -10,9 +10,9 @@ import (
// RequestContext contains the needed information to make handlers work.
type RequestContext struct {
*FileManager
User *User
FM *FileManager
FI *file
File *file
// On API handlers, Router is the APi handler we want.
Router string
}
@@ -21,9 +21,9 @@ type RequestContext struct {
func serveHTTP(c *RequestContext, 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!
p := strings.TrimPrefix(r.URL.Path, c.FM.BaseURL)
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
if len(p) >= len(r.URL.Path) && c.FM.BaseURL != "" {
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
return http.StatusNotFound, nil
}
@@ -34,7 +34,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
if r.URL.Path == "/sw.js" {
return renderFile(
w,
c.FM.assets.MustString("sw.js"),
c.assets.MustString("sw.js"),
"application/javascript",
c,
)
@@ -65,7 +65,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return renderFile(
w,
c.FM.assets.MustString("index.html"),
c.assets.MustString("index.html"),
"text/html",
c,
)
@@ -74,13 +74,13 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
// staticHandler handles the static assets path.
func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.FM.assets.HTTPBox()).ServeHTTP(w, r)
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil
}
return renderFile(
w,
c.FM.assets.MustString("static/manifest.json"),
c.assets.MustString("static/manifest.json"),
"application/json",
c,
)
@@ -107,8 +107,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return http.StatusForbidden, nil
}
for _, p := range c.FM.Plugins {
code, err := p.BeforeAPI(c, w, r)
for p := range c.Plugins {
code, err := plugins[p].Handler.Before(c, w, r)
if code != 0 || err != nil {
return code, err
}
@@ -116,7 +116,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
if c.Router == "checksum" || c.Router == "download" {
var err error
c.FI, err = getInfo(r.URL, c.FM, c.User)
c.File, err = getInfo(r.URL, c.FileManager, c.User)
if err != nil {
return errorToHTTP(err, false), err
}
@@ -138,10 +138,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
code, err = resourceHandler(c, w, r)
case "users":
code, err = usersHandler(c, w, r)
case "commands":
code, err = commandsHandler(c, w, r)
case "plugins":
code, err = pluginsHandler(c, w, r)
case "settings":
code, err = settingsHandler(c, w, r)
default:
code = http.StatusNotFound
}
@@ -150,8 +148,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return code, err
}
for _, p := range c.FM.Plugins {
code, err := p.AfterAPI(c, w, r)
for p := range c.Plugins {
code, err := plugins[p].Handler.After(c, w, r)
if code != 0 || err != nil {
return code, err
}
@@ -164,7 +162,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo")
val, err := c.FI.Checksum(query)
val, err := c.File.Checksum(query)
if err == errInvalidOption {
return http.StatusBadRequest, err
} else if err != nil {
@@ -194,18 +192,17 @@ func splitURL(path string) (string, string) {
// renderFile renders a file using a template with some needed variables.
func renderFile(w http.ResponseWriter, file string, contentType string, c *RequestContext) (int, error) {
functions := template.FuncMap{
"JS": func(s string) template.JS {
return template.JS(s)
},
}
tpl := template.Must(template.New("file").Funcs(functions).Parse(file))
tpl := template.Must(template.New("file").Parse(file))
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
var javascript = ""
for name := range c.Plugins {
javascript += plugins[name].JavaScript + "\n"
}
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.FM.RootURL(),
"Plugins": c.FM.Plugins,
"BaseURL": c.RootURL(),
"JavaScript": template.JS(javascript),
})
if err != nil {
return http.StatusInternalServerError, err

View File

@@ -14,53 +14,57 @@
"moment": "^2.18.1",
"normalize.css": "^7.0.0",
"vue": "^2.3.3",
"vue-i18n": "^7.1.0",
"vue-router": "^2.7.0",
"vuex": "^2.3.1"
},
"devDependencies": {
"autoprefixer": "^6.7.2",
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"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": "^1.1.3",
"chalk": "^2.0.1",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"eslint": "^3.19.0",
"eslint-config-standard": "^6.2.1",
"eslint-friendly-formatter": "^2.0.7",
"eslint": "^4.3.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-html": "^2.0.0",
"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-plugin-standard": "^2.0.1",
"eslint-plugin-standard": "^3.0.1",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"extract-text-webpack-plugin": "^2.0.0",
"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",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"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",
"sw-precache-webpack-plugin": "^0.9.1",
"sw-precache-webpack-plugin": "^0.11.4",
"uglify-js": "^3.0.23",
"url-loader": "^0.5.8",
"vue-loader": "^12.1.0",
"vue-loader": "^13.0.2",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.3.3",
"webpack": "^2.6.1",
"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"
"webpack-merge": "^4.1.0",
"yml-loader": "^2.1.0"
},
"engines": {
"node": ">= 4.0.0",

228
plugins/hugo.go Normal file
View File

@@ -0,0 +1,228 @@
package plugins
import (
"errors"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/hacdias/filemanager"
"github.com/hacdias/varutils"
"github.com/robfig/cron"
)
func init() {
filemanager.RegisterPlugin("hugo", filemanager.Plugin{
JavaScript: hugoJavaScript,
CommandEvents: []string{"before_publish", "after_publish"},
Permissions: []filemanager.Permission{
{
Name: "allowPublish",
Value: true,
},
},
Handler: &hugo{},
})
}
var (
ErrHugoNotFound = errors.New("It seems that tou don't have 'hugo' on your PATH")
ErrUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
)
// Hugo is a hugo (https://gohugo.io) plugin.
type Hugo struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Hugo executable path
Exe string `name:"Hugo Executable"`
// Hugo arguments
Args []string `name:"Hugo Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
}
// Find finds the hugo executable in the path.
func (h *Hugo) Find() error {
var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil {
return ErrHugoNotFound
}
return nil
}
// run runs Hugo with the define arguments.
func (h Hugo) run(force bool) {
// If the CleanPublic option is enabled, clean it.
if h.CleanPublic {
os.RemoveAll(h.Public)
}
// Prevent running if watching is enabled
if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force {
if len(h.Args) > pos && h.Args[pos+1] != "false" {
return
}
if len(h.Args) == pos+1 {
return
}
}
if err := Run(h.Exe, h.Args, h.Root); err != nil {
log.Println(err)
}
}
// schedule schedules a post to be published later.
func (h Hugo) schedule(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
t, err := time.Parse("2006-01-02T15:04", r.Header.Get("Schedule"))
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
path = filepath.Clean(path)
if err != nil {
return http.StatusInternalServerError, err
}
scheduler := cron.New()
scheduler.AddFunc(t.Format("05 04 15 02 01 *"), func() {
if err := h.undraft(path); err != nil {
log.Printf(err.Error())
return
}
h.run(false)
})
scheduler.Start()
return http.StatusOK, nil
}
func (h Hugo) undraft(file string) error {
args := []string{"undraft", file}
if err := Run(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") {
return err
}
return nil
}
type hugo struct{}
func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
o := c.Plugins["hugo"].(*Hugo)
// If we are using the 'magic url' for the settings, we should redirect the
// request for the acutual path.
if r.URL.Path == "/settings/" || r.URL.Path == "/settings" {
var frontmatter string
var err error
if _, err = os.Stat(filepath.Join(o.Root, "config.yaml")); err == nil {
frontmatter = "yaml"
}
if _, err = os.Stat(filepath.Join(o.Root, "config.json")); err == nil {
frontmatter = "json"
}
if _, err = os.Stat(filepath.Join(o.Root, "config.toml")); err == nil {
frontmatter = "toml"
}
r.URL.Path = "/config." + frontmatter
}
// From here on, we only care about 'hugo' router so we can bypass
// the others.
if c.Router != "hugo" {
return 0, nil
}
// If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method.
if r.Method != http.MethodPost {
return http.StatusMethodNotAllowed, nil
}
// If we are creating a file built from an archetype.
if r.Header.Get("Archetype") != "" {
if !c.User.AllowNew {
return http.StatusForbidden, nil
}
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
archetype := r.Header.Get("archetype")
ext := filepath.Ext(filename)
// If the request isn't for a markdown file, we can't
// handle it.
if ext != ".markdown" && ext != ".md" {
return http.StatusBadRequest, ErrUnsupportedFileType
}
// Tries to create a new file based on this archetype.
args := []string{"new", filename, "--kind", archetype}
if err := Run(o.Exe, args, o.Root); err != nil {
return http.StatusInternalServerError, err
}
// Writes the location of the new file to the Header.
w.Header().Set("Location", "/files/content/"+filename)
return http.StatusCreated, nil
}
// If we are trying to regenerate the website.
if r.Header.Get("Regenerate") == "true" {
if !c.User.Permissions["allowPublish"] {
return http.StatusForbidden, nil
}
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// Before save command handler.
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
// We only run undraft command if it is a file.
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
if err := o.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
}
// Regenerates the file
o.run(false)
// Executed the before publish command.
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
if r.Header.Get("Schedule") != "" {
if !c.User.Permissions["allowPublish"] {
return http.StatusForbidden, nil
}
return o.schedule(c, w, r)
}
return http.StatusNotFound, nil
}
func (h hugo) After(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}

View File

@@ -1,4 +1,6 @@
'use strict';
package plugins
const hugoJavaScript = `'use strict';
(function () {
if (window.plugins === undefined || window.plugins === null) {
@@ -10,8 +12,8 @@
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${data.store.state.baseURL}/api/hugo${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`)
request.open('POST', data.store.state.baseURL + "/api/hugo" + url, true)
request.setRequestHeader('Authorization', "Bearer " + data.store.state.jwt)
request.setRequestHeader('Regenerate', 'true')
request.onload = () => {
@@ -32,8 +34,8 @@
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${data.store.state.baseURL}/api/hugo${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`)
request.open('POST', data.store.state.baseURL + "/api/hugo" + url, true)
request.setRequestHeader('Authorization',"Bearer " + data.store.state.jwt)
request.setRequestHeader('Archetype', encodeURIComponent(type))
request.onload = () => {
@@ -54,8 +56,8 @@
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${data.store.state.baseURL}/api/hugo${file}`, true)
request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`)
request.open('POST', data.store.state.baseURL + "/api/hugo" + file, true)
request.setRequestHeader('Authorization', "Bearer " + data.store.state.jwt)
request.setRequestHeader('Schedule', date)
request.onload = () => {
@@ -80,8 +82,6 @@
if: function (data, route) {
return (data.store.state.req.kind === 'editor' &&
!data.store.state.loading &&
data.store.state.req.metadata !== undefined &&
data.store.state.req.metadata !== null &&
data.store.state.user.allowEdit &
data.store.state.user.permissions.allowPublish)
},
@@ -224,4 +224,4 @@
}
]
})
})()
})()`

View File

@@ -1,4 +1,4 @@
package hugo
package plugins
import (
"errors"

View File

@@ -10,13 +10,13 @@ import (
"path/filepath"
"strings"
"github.com/hacdias/filemanager/dir"
"github.com/hacdias/fileutils"
)
// sanitizeURL sanitizes the URL to prevent path transversal
// using dir.SlashClean and adds the trailing slash bar.
// using fileutils.SlashClean and adds the trailing slash bar.
func sanitizeURL(url string) string {
path := dir.SlashClean(url)
path := fileutils.SlashClean(url)
if strings.HasSuffix(url, "/") && path != "/" {
return path + "/"
}
@@ -34,7 +34,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
case http.MethodPut:
// Before save command handler.
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
if err := c.FM.Runner("before_save", path); err != nil {
if err := c.Runner("before_save", path); err != nil {
return http.StatusInternalServerError, err
}
@@ -44,7 +44,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// After save command handler.
if err := c.FM.Runner("after_save", path); err != nil {
if err := c.Runner("after_save", path); err != nil {
return http.StatusInternalServerError, err
}
@@ -60,7 +60,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file.
f, err := getInfo(r.URL, c.FM, c.User)
f, err := getInfo(r.URL, c.FileManager, c.User)
if err != nil {
return errorToHTTP(err, false), err
}
@@ -73,7 +73,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
// If it is a dir, go and serve the listing.
if f.IsDir {
c.FI = f
c.File = f
return listingHandler(c, w, r)
}
@@ -101,7 +101,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.FI
f := c.File
f.Kind = "listing"
// Tries to get the listing data.
@@ -112,7 +112,7 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
listing := f.listing
// Defines the cookie scope.
cookieScope := c.FM.RootURL()
cookieScope := c.RootURL()
if cookieScope == "" {
cookieScope = "/"
}

View File

@@ -1 +1 @@
be97c942365e7a765698a8c0175b4eec18cb4899
2678061f4a5fd54d606c3bbaa3aa899a5e55cf37

View File

@@ -2,103 +2,128 @@ package filemanager
import (
"encoding/json"
"errors"
"net/http"
"reflect"
"github.com/mitchellh/mapstructure"
)
func commandsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
switch r.Method {
case http.MethodGet:
return commandsGetHandler(c, w, r)
case http.MethodPut:
return commandsPutHandler(c, w, r)
}
return http.StatusMethodNotAllowed, nil
type modifySettingsRequest struct {
*modifyRequest
Data struct {
Commands map[string][]string `json:"commands"`
Plugins map[string]map[string]interface{} `json:"plugins"`
} `json:"data"`
}
func commandsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
return renderJSON(w, c.FM.Commands)
type pluginOption struct {
Variable string `json:"variable"`
Name string `json:"name"`
Value interface{} `json:"value"`
}
func commandsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request body is empty.
if r.Body == nil {
return http.StatusBadGateway, errors.New("Empty request body")
return nil, errEmptyRequest
}
var commands map[string][]string
// Parses the user and checks for error.
err := json.NewDecoder(r.Body).Decode(&commands)
// Parses the request body and checks if it's well formed.
mod := &modifySettingsRequest{}
err := json.NewDecoder(r.Body).Decode(mod)
if err != nil {
return http.StatusBadRequest, errors.New("Invalid JSON")
return nil, err
}
if err := c.FM.db.Set("config", "commands", commands); err != nil {
return http.StatusInternalServerError, err
// Checks if the request type is right.
if mod.What != "settings" {
return nil, errWrongDataType
}
c.FM.Commands = commands
return http.StatusOK, nil
return mod, nil
}
func pluginsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "" && r.URL.Path != "/" {
return http.StatusNotFound, nil
}
switch r.Method {
case http.MethodGet:
return pluginsGetHandler(c, w, r)
return settingsGetHandler(c, w, r)
case http.MethodPut:
return pluginsPutHandler(c, w, r)
return settingsPutHandler(c, w, r)
}
return http.StatusMethodNotAllowed, nil
}
func pluginsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
return renderJSON(w, c.FM.Plugins)
type settingsGetRequest struct {
Commands map[string][]string `json:"commands"`
Plugins map[string][]pluginOption `json:"plugins"`
}
func pluginsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
if r.Body == nil {
return http.StatusBadGateway, errors.New("Empty request body")
result := &settingsGetRequest{
Commands: c.Commands,
Plugins: map[string][]pluginOption{},
}
var raw map[string]map[string]interface{}
for name, p := range c.Plugins {
result.Plugins[name] = []pluginOption{}
// Parses the user and checks for error.
err := json.NewDecoder(r.Body).Decode(&raw)
t := reflect.TypeOf(p).Elem()
for i := 0; i < t.NumField(); i++ {
result.Plugins[name] = append(result.Plugins[name], pluginOption{
Variable: t.Field(i).Name,
Name: t.Field(i).Tag.Get("name"),
Value: reflect.ValueOf(p).Elem().FieldByName(t.Field(i).Name).Interface(),
})
}
}
return renderJSON(w, result)
}
func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
mod, err := parsePutSettingsRequest(r)
if err != nil {
return http.StatusBadRequest, err
}
for name, plugin := range raw {
err = mapstructure.Decode(plugin, c.FM.Plugins[name])
if err != nil {
// Update the commands.
if mod.Which == "commands" {
if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil {
return http.StatusInternalServerError, err
}
err = c.FM.db.Set("plugins", name, c.FM.Plugins[name])
if err != nil {
return http.StatusInternalServerError, err
}
c.Commands = mod.Data.Commands
return http.StatusOK, nil
}
return http.StatusOK, nil
// Update the plugins.
if mod.Which == "plugins" {
for name, plugin := range mod.Data.Plugins {
err = mapstructure.Decode(plugin, c.Plugins[name])
if err != nil {
return http.StatusInternalServerError, err
}
err = c.db.Set("plugins", name, c.Plugins[name])
if err != nil {
return http.StatusInternalServerError, err
}
}
return http.StatusOK, nil
}
return http.StatusMethodNotAllowed, nil
}

214
users.go
View File

@@ -11,20 +11,22 @@ import (
"github.com/asdine/storm"
)
type modifyRequest struct {
What string `json:"what"` // Answer to: what data type?
Which string `json:"which"` // Answer to: which field?
}
type modifyUserRequest struct {
*modifyRequest
Data *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) {
if r.URL.Path == "/change-password" {
return usersUpdatePassword(c, w, r)
}
if r.URL.Path == "/change-css" {
return usersUpdateCSS(c, w, r)
}
// If the user is admin and the HTTP Method is not
// PUT, then we return forbidden.
if !c.User.Admin {
// If the user isn't admin and isn't making a PUT
// request, then return forbidden.
if !c.User.Admin && r.Method != http.MethodPut {
return http.StatusForbidden, nil
}
@@ -61,32 +63,38 @@ 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, error) {
func getUser(r *http.Request) (*User, string, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, errEmptyRequest
return nil, "", errEmptyRequest
}
u := &User{}
err := json.NewDecoder(r.Body).Decode(u)
// Parses the request body and checks if it's well formed.
mod := &modifyUserRequest{}
err := json.NewDecoder(r.Body).Decode(mod)
if err != nil {
return nil, err
return nil, "", err
}
return u, nil
// Checks if the request type is right.
if mod.What != "user" {
return nil, "", errWrongDataType
}
return mod.Data, mod.Which, nil
}
func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Request for the default user data.
if r.URL.Path == "/base" {
return renderJSON(w, c.FM.DefaultUser)
return renderJSON(w, c.DefaultUser)
}
// Request for the listing of users.
if r.URL.Path == "/" {
users := []User{}
for _, user := range c.FM.Users {
for _, user := range c.Users {
// Copies the user info and removes its
// password so it won't be sent to the
// front-end.
@@ -108,7 +116,7 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Searches for the user and prints the one who matches.
for _, user := range c.FM.Users {
for _, user := range c.Users {
if user.ID != id {
continue
}
@@ -127,11 +135,26 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusMethodNotAllowed, nil
}
u, err := getUser(r)
u, _, err := getUser(r)
if err != nil {
return http.StatusBadRequest, err
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername
}
// Checks if filesystem isn't empty.
if u.FileSystem == "" {
return http.StatusBadRequest, 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")
@@ -161,7 +184,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.Password = pw
// Saves the user to the database.
err = c.FM.db.Save(u)
err = c.db.Save(u)
if err == storm.ErrAlreadyExists {
return http.StatusConflict, errUserExist
}
@@ -171,7 +194,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Saves the user to the memory.
c.FM.Users[u.Username] = u
c.Users[u.Username] = u
// Set the Location header and return.
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
@@ -190,7 +213,7 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
// Deletes the user from the database.
err = c.FM.db.DeleteStruct(&User{ID: id})
err = c.db.DeleteStruct(&User{ID: id})
if err == storm.ErrNotFound {
return http.StatusNotFound, errUserNotExist
}
@@ -200,9 +223,9 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
// Delete the user from the in-memory users map.
for _, user := range c.FM.Users {
for _, user := range c.Users {
if user.ID == id {
delete(c.FM.Users, user.Username)
delete(c.Users, user.Username)
break
}
}
@@ -210,72 +233,79 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusOK, nil
}
func usersUpdatePassword(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != http.MethodPut {
return http.StatusMethodNotAllowed, nil
}
u, err := getUser(r)
if err != nil {
return http.StatusBadRequest, err
}
if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword
}
pw, err := hashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
c.User.Password = pw
err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func usersUpdateCSS(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != http.MethodPut {
return http.StatusMethodNotAllowed, nil
}
u, err := getUser(r)
if err != nil {
return http.StatusBadRequest, err
}
c.User.CSS = u.CSS
err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// New users should be created on /api/users.
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil
}
// Gets the user ID from the URL and checks if it's valid.
id, err := getUserID(r)
if err != nil {
return http.StatusInternalServerError, err
}
u, err := getUser(r)
// Checks if the user has permission to access this page.
if !c.User.Admin && id != c.User.ID {
return http.StatusForbidden, nil
}
// Gets the user from the request body.
u, which, err := getUser(r)
if err != nil {
return http.StatusBadRequest, err
}
// The username and the filesystem cannot be empty.
if u.Username == "" || u.FileSystem == "" {
return http.StatusBadRequest, errors.New("Username, password or scope are empty")
// 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
}
err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
// Updates the Password.
if which == "password" {
if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword
}
pw, err := 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)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
// If can only be all.
if which != "all" {
return http.StatusBadRequest, errInvalidUpdateField
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername
}
// Checks if filesystem isn't empty.
if u.FileSystem == "" {
return http.StatusBadRequest, errEmptyScope
}
// Initialize rules if they're not initialized.
@@ -288,48 +318,50 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.Commands = []string{}
}
var ouser *User
for _, user := range c.FM.Users {
// Gets the current saved user from the in-memory map.
var suser *User
for _, user := range c.Users {
if user.ID == id {
ouser = user
suser = user
break
}
}
if ouser == nil {
if suser == nil {
return http.StatusNotFound, nil
}
u.ID = id
if u.Password == "" {
u.Password = ouser.Password
} else {
// Changes the password if the request wants it.
if u.Password != "" {
pw, err := hashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = pw
} else {
u.Password = suser.Password
}
// Default permissions if current are nil.
if u.Permissions == nil {
u.Permissions = c.FM.DefaultUser.Permissions
u.Permissions = c.DefaultUser.Permissions
}
// Updates the whole User struct because we always are supposed
// to send a new entire object.
err = c.FM.db.Save(u)
err = c.db.Save(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 ouser.Username != u.Username {
delete(c.FM.Users, ouser.Username)
if suser.Username != u.Username {
delete(c.Users, suser.Username)
}
c.FM.Users[u.Username] = u
c.Users[u.Username] = u
return http.StatusOK, nil
}

View File

@@ -1,13 +0,0 @@
package variables
import "reflect"
// IsMap checks if some variable is a map
func IsMap(sth interface{}) bool {
return reflect.ValueOf(sth).Kind() == reflect.Map
}
// IsSlice checks if some variable is a slice
func IsSlice(sth interface{}) bool {
return reflect.ValueOf(sth).Kind() == reflect.Slice
}

View File

@@ -1,49 +0,0 @@
package variables
import "testing"
type interfaceToBool struct {
Value interface{}
Result bool
}
var testIsMap = []*interfaceToBool{
{"teste", false},
{453478, false},
{-984512, false},
{true, false},
{map[string]bool{}, true},
{map[int]bool{}, true},
{map[interface{}]bool{}, true},
{[]string{}, false},
}
func TestIsMap(t *testing.T) {
for _, test := range testIsMap {
if IsMap(test.Value) != test.Result {
t.Errorf("Incorrect value on IsMap for %v; want: %v; got: %v", test.Value, test.Result, !test.Result)
}
}
}
var testIsSlice = []*interfaceToBool{
{"teste", false},
{453478, false},
{-984512, false},
{true, false},
{map[string]bool{}, false},
{map[int]bool{}, false},
{map[interface{}]bool{}, false},
{[]string{}, true},
{[]int{}, true},
{[]bool{}, true},
{[]interface{}{}, true},
}
func TestIsSlice(t *testing.T) {
for _, test := range testIsSlice {
if IsSlice(test.Value) != test.Result {
t.Errorf("Incorrect value on IsSlice for %v; want: %v; got: %v", test.Value, test.Result, !test.Result)
}
}
}

View File

@@ -1,47 +0,0 @@
package variables
import (
"errors"
"log"
"reflect"
)
// Dict allows to send more than one variable into a template.
func Dict(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
}
// FieldInStruct checks if variable is defined in a struct.
func FieldInStruct(data interface{}, field string) bool {
t := reflect.Indirect(reflect.ValueOf(data)).Type()
if t.Kind() != reflect.Struct {
log.Print("Non-struct type not allowed.")
return false
}
_, b := t.FieldByName(field)
return b
}
// StringInSlice checks if a slice contains a string.
func StringInSlice(a string, list []string) (bool, int) {
for i, b := range list {
if b == a {
return true, i
}
}
return false, 0
}

View File

@@ -1,41 +0,0 @@
package variables
import "testing"
type testFieldInStructData struct {
f1 string
f2 bool
f3 int
f4 func()
}
type testFieldInStruct struct {
data interface{}
field string
result bool
}
var testFieldInStructCases = []testFieldInStruct{
{testFieldInStructData{}, "f1", true},
{testFieldInStructData{}, "f2", true},
{testFieldInStructData{}, "f3", true},
{testFieldInStructData{}, "f4", true},
{testFieldInStructData{}, "f5", false},
{[]string{}, "", false},
{map[string]int{"oi": 4}, "", false},
{"asa", "", false},
{"int", "", false},
}
func TestFieldInStruct(t *testing.T) {
for _, pair := range testFieldInStructCases {
v := FieldInStruct(pair.data, pair.field)
if v != pair.result {
t.Error(
"For", pair.data,
"expected", pair.result,
"got", v,
)
}
}
}

View File

@@ -2,6 +2,7 @@ package filemanager
import (
"bytes"
"encoding/json"
"mime"
"net/http"
"os"
@@ -321,7 +322,12 @@ func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, err
}
}
return conn.WriteMessage(websocket.TextMessage, []byte(path))
response, _ := json.Marshal(map[string]interface{}{
"dir": f.IsDir(),
"path": path,
})
return conn.WriteMessage(websocket.TextMessage, response)
})
if err != nil {