Compare commits

...

114 Commits

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

* Add after_upload and after_delete trigger

* Fix scope since merge with master

* Add new before triggers


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

* ldflags aren't need anymore


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

* i18n: update locale


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

* add french language file fr.yaml


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

* i18n: add zh-tw.yaml


Former-commit-id: e9acae3ae0606e998f49580c747d825d6cee2d3b [formerly 636069d89a37f774b715cbbb4a7989dc23d44feb] [formerly 851dccd385f04b15a9add0b0bab94c6b14d78710 [formerly b35dddc8b5]]
Former-commit-id: 1a48d307e3d81ce78d71ea0983b68e080a56bd0d [formerly a1bc2290c3578a770593baa95dd5e399d8b6b862]
Former-commit-id: 41eb7b7fdb4bb1a446012e1e35b35e664c371e78
2017-08-12 16:25:53 +01:00
Henrique Dias
4c8b118848 build assets
Former-commit-id: 42beb38910029a0e3769d827c5b5e97a834d8df9 [formerly bcd3b09ec9cec7d6a38e61f82f191c7e792fa113] [formerly 5c8b4845e0f0270cc92d6c7f3f1266419fcb09f8 [formerly c631c28ed4]]
Former-commit-id: f4a247938b039c1c5bc4e21ad51dd31d17692d73 [formerly a29862a918fa148f3ab0b93d3cf8f789af16b5be]
Former-commit-id: edd88a96aa4da4e9e92ea93ace068d72431c2f01
2017-08-12 10:22:51 +01:00
Equim
146744ea93 Add Japanese localization, QC Chinese. (#200)
* i18n: add ja.yaml

* i18n: polish zh-cn.yaml, apply ja.yaml to i18n

* i18n: use the languages themselves to describe languges


Former-commit-id: 4496525cc830c216264c9522569648f75413267c [formerly 552b9547f59779ae5c8d2a856ece402da9dcd509] [formerly 85f0848675510d3b25bb991eb75ac10e04cdf3d9 [formerly 7526f72379]]
Former-commit-id: 73a84da80139e9f8be77e39f620165dc938f6820 [formerly 8178a9cba22362fdf4fa29331fd07127169e8980]
Former-commit-id: 660bee270208abeebfca96fff2adba62e1bccbf3
2017-08-12 10:17:34 +01:00
Henrique Dias
689077c545 update dependency
Former-commit-id: 07c998af23f952c0c14cbcf7b3510b5f7d4a0b3a [formerly 4b4f53e1ec057174bd857618f4a9c9d3f66b98e5] [formerly 9167e678ebd65ae5a18d9a911b61985b087cc57e [formerly 9f28c09ba7]]
Former-commit-id: 768ed62144d735dca235a95c2751e58acc2e81f6 [formerly 37c79666ade404f030b116bacd1805572aeb7824]
Former-commit-id: bf9ff92e59eac24298635755f37a003c064501b4
2017-08-11 17:45:30 +01:00
Henrique Dias
53bba54ce4 build assets
Former-commit-id: a30f3f489c14751caa5ece6a63529913816c88ef [formerly eb03631e122c07085bfea546816ac2828d34eab5] [formerly f119bf3a2c0f746a4262c21fa822f22e6826a6a7 [formerly 8b7fbbb59d]]
Former-commit-id: 247e71bbdbc8d22806e2d57be2dae5079e830598 [formerly ec18ed9cc7f41340b2e991bea0d0acdf018941d8]
Former-commit-id: d63ae14236dfcc0c5a843d314cc763182124cb08
2017-08-11 14:25:41 +01:00
Toyo
d8974ce3af Update zh-cn.yaml (#198)
Former-commit-id: 145db15ed8cbedab1391981375936202204f8ce5 [formerly 030bcc8fe59b517e1c18c28703b9efb79f6d954e] [formerly d8373763e96110a3c89d3dad777044ed628ca2e0 [formerly 081060c602]]
Former-commit-id: 498af8fd125356eb31f7df844c47193ef5abc11c [formerly 0b7b6a2e27f79e9cb0ea6f68fa2571b7f144d4d6]
Former-commit-id: 5cf37605c852bd4e34930bdd72001777c2cb607b
2017-08-11 14:08:10 +01:00
Henrique Dias
8d715bb433 change new folder and file permissions #190
progresses on sharing #192

Progresses on #192

Progresses on #192

Little API update

Build assets


Former-commit-id: 68e70132ea857eb65638c0496c030be1c181ed1c [formerly d67b74280b7f12c3e20de6abe31fcfc26e8f43ef] [formerly 8fe91e003c9616da23f0e673ad4bb89d792a41c8 [formerly 8684343605]]
Former-commit-id: 7d22ff468e580601d0c3e0921734b587b92484f8 [formerly 55f9d830636f9bbf15e0453d1ee7de6ee5d5191e]
Former-commit-id: ad411a5979521dda9ea9683d86e4c8ae7b3c9e6f
2017-08-11 11:55:08 +01:00
Henrique Dias
25a86a9382 Simplify Static Generators Code; progresses on #108
Former-commit-id: 7f4552b70980f39204e5a5517a1c1f3602304569 [formerly 0c9cdc2a1e988d378f8a068b9a6b53eb41144f2f] [formerly e221c08add31a6837c8695c413b2c55d31806f16 [formerly 7ee721a9e3]]
Former-commit-id: de2ff86dbc24a8f77260539d656201daf9dfebe5 [formerly 3d0eb1e810bbce97a7bf430abf0d0c7a3833ce2a]
Former-commit-id: 6f22d477a1105c7b84cbe70dda1afb0d364e47c5
2017-08-10 15:04:07 +01:00
Henrique Dias
97f31310c6 Remove useless file.
Former-commit-id: 320714ef9b83cae1b622f7398683bd6fd8e0f6e6 [formerly 09e30a738673c39a609d8313e49c6793961da308] [formerly afc89ef1bd0ba7c9b1146d675c17e0ad27b8fec2 [formerly 19643be237]]
Former-commit-id: 1e37ec28bfd97b202a24f1d61a1324d54aaad35a [formerly 90067dc4f5712813b97412e9073f1bfbc62174de]
Former-commit-id: 7dd2f8736ee986d0f51c6e82cbf784fe954d0316
2017-08-10 14:45:49 +01:00
Toyo
97b5cea521 Update zh-cn.yaml (#196)
Former-commit-id: 639f0d80b458bc856e3b08168f737154a4c63541 [formerly 4ac7c12544312a19326df1ed5c4047082384f30b] [formerly 1a4604ab0ae845250dd6fd8adf72ab5d2c028eb5 [formerly d141f0efa9]]
Former-commit-id: 291f4ebe0687f5dd3111fc77c68f2c582a2f5d1a [formerly cd9ad15e41343fcb969ec48f2608c00276231faf]
Former-commit-id: 607fef4cc1c23c45dbb4563e8323a4fe74643dcd
2017-08-10 14:07:31 +01:00
Henrique Dias
2462346e56 Jekyll; progresses on #108
Former-commit-id: 05caf8c6dd2df09d064a14c034e221151a2dd341 [formerly df5cbcf51aa8a2e76e39d73f30f4e63f222bd70f] [formerly 3fbf8281b93fed6c16cd7c8f810dccdf1f7347d6 [formerly fe80e98f85]]
Former-commit-id: b1d034b6c7ee1ce445bd53039ce0306f2b2b7651 [formerly 7b3c5571c7647ae4d4f25f46e66df49aa0e3320c]
Former-commit-id: 0f57d274deaeae52b9da52d8f6495070caba6ee7
2017-08-10 12:18:51 +01:00
Henrique Dias
aaf6d60c3c close #195
Former-commit-id: 2491dc59e82845c50ece7d7b0753dce2b0484e3f [formerly 57ded9ff433cedf0011fedbbd6a1c8963103d9a7] [formerly 70bad58f0dc863e1885fb486d9c02be8a6e3ca43 [formerly e533a3aa6e]]
Former-commit-id: 2481eaceaba80955dea0552aacdef58937069edb [formerly b4890a9b2147c7410706604563188f1d3b1e8b56]
Former-commit-id: 7354ce9185a8e63fb5993246ea0f357f2aa6d358
2017-08-10 11:21:06 +01:00
Henrique Dias
3ff8908047 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: f9634969196ff80e104683aa61c26cf66628a5fd [formerly e3c814a02bd466ecfbf80e52e14129493f3af832] [formerly a00f02e4e8fdaf5f752dbdb058cbc53c76ea8df4 [formerly 567e24231f]]
Former-commit-id: 28b08e7a12774dc652a987cf2dbb9389ef575043 [formerly 89b5eb8928251ef4a045627ff8aad1db1f1d918d]
Former-commit-id: ab3d78a1a2a8aebaa0006d96992d2858044ba243
2017-08-10 11:09:06 +01:00
Henrique Dias
d5e943069e StaticGen update Bash
Update zh-cn.yaml (#194)

Address #184

build assets

Update zh-cn.yaml (#194)


Former-commit-id: 4572f93371647c0a6b53cc375ec5bb00356a37c9 [formerly e58e4793ac0ca6915b605d7b2a8a77f0aec31172] [formerly 43635f6b98f546ec0e2656d26031388aef63a902 [formerly da4fd84002]]
Former-commit-id: 15422887ad29dea63bdf861d9da8f8d28b4fbc8f [formerly 3949ffa499cf999c3f4b50ee18802c0f87a23807]
Former-commit-id: fac5ceeee3fa969239d9ef5e04ef5542a61d2761
2017-08-10 11:08:50 +01:00
Toyo
30be924648 Update zh-cn.yaml (#194)
Former-commit-id: 811ff539f580c6e5e926f7000a1ad0152c58142a [formerly 58cb75b9fc4f1022b26f6b09016ae0b5a06b7e41] [formerly 1e6fb8f5112a4eccbc469da1557bb15586fddd03 [formerly c59ac7e321]]
Former-commit-id: c7967b4572973a609997e49541628695a40143cf [formerly 331933c8f014500698e925ea6e527bf2330a8953]
Former-commit-id: ee537e70514fe5507ff985a0816f6e7ae7d06dcb
2017-08-09 08:34:28 +01:00
Henrique Dias
bfdb924cb7 Search improvements #156
Former-commit-id: 131b9e565750205e39b9b2d029bff895b4cdbe30 [formerly 1c848718a1f743c9c74a1170cb932eaf7ae8584b] [formerly b856d5d960edf7f40dfb0f15be91dc2b8cbf4c06 [formerly 9819c87853]]
Former-commit-id: 332be2031e391993df55e705250e7459f9227877 [formerly de5fcf78ac347621ff60ba89c72a3f88efdc0f5e]
Former-commit-id: 57f767bf0ea685f3f147b57b441e8cb2b048ea18
2017-08-08 11:33:10 +01:00
Henrique Dias
6d853d63ed Hugo preview mode. Update Plugin registration.
Former-commit-id: b6c33f0ae36e7caa8cf813eeb6f8fdeb554659e9 [formerly 4d62c13f0b7e5b7610a0c9037d646c2005e9ba88] [formerly 1e9ded0427d81084c92a68d5eddb8c7620fb189c [formerly ae8eaf96c4]]
Former-commit-id: 76699219a6d176777f4478eab5141bba5a613a17 [formerly bb698073213359bebce84ea196b30e9467f448c0]
Former-commit-id: 538927b5a6fad024eb062cf746068372c384b883
2017-08-08 10:21:25 +01:00
Henrique Dias
4bae90c80f Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 24a9bcd2de3e275c99da8eaa091b98865a82fd32 [formerly 596230fe6cf428f04822f5af000ddee429568c3a] [formerly fc7ec7bc4eb116e9afc04d4f7eae428052a91958 [formerly a2ed744f24]]
Former-commit-id: a45873320fb58f0abc1629341dbf97c96ed65cb9 [formerly 759add9806b1599e889133973b7e39919862accc]
Former-commit-id: 71acac2d754874a22dffd8f0b601465e2308a211
2017-08-08 09:19:11 +01:00
Henrique Dias
e464eb69ed fix #193
Former-commit-id: 5765a9c1178a015e036481e6eb78f498bead5b04 [formerly 37680ed0dab2e85b3ac921fbe3fd354a238211f5] [formerly ed9fbac269e054784e5f375c71c6423441667e82 [formerly e42ec4b42f]]
Former-commit-id: 5877cfd762b4dc1e6924ae87f136f24c82a98fdc [formerly 65376cc4c37750b9e7efdfae8fe24687a683d057]
Former-commit-id: 4063ea78f117a8e3d04a047280e8462c6d5d493e
2017-08-08 09:18:24 +01:00
Toyo
fee9330cfa Update zh-cn.yaml (#191)
Former-commit-id: e3cddc3fb1540e5308e6aca310e1fee7b7d18c9e [formerly a7b5a23715965b972e7d9ea97a6349388ca21df3] [formerly 339be0b244508d0c88a76e271e9ada949d41f3c4 [formerly dc7f389fde]]
Former-commit-id: 631ab6a08944f2e861a37aa59e2c01348a0a59f9 [formerly 96e267b39b21d6beb5693ce11c92d802512625d3]
Former-commit-id: 533b10726159061723dfce3511fb11bd4f167789
2017-08-07 18:07:21 +01:00
Henrique Dias
b034c87ec0 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 599633a56b78c94855f0e75ec76823a5670e61db [formerly 60ef68d1eef84903835db5de79a02f5554bc9a84] [formerly b96770a982b1fcbf5c4fffa00978e535e921bff2 [formerly 40fda1368a]]
Former-commit-id: c79c723954d8a5ac26f9f7594396fd2fd740c1e4 [formerly b103a905107b1f72f7c98aaf309b591f4429fb7d]
Former-commit-id: e40b3de8dfd04947c0b037a854f04ccd81e88fa4
2017-08-07 14:48:05 +01:00
Henrique Dias
87420bcefd Build assets
Former-commit-id: fa20c1e669918db4bfd51f5d12785923392ca966 [formerly 24061bfbf3dc8f0a9f42b929d68946e4969db5f7] [formerly 3450f5d2eb248b7dfe88616c2d294cee725ccf92 [formerly 231f3f46ac]]
Former-commit-id: b215d97223440354b9bacbaf467da269839880f0 [formerly c56116a6a6e8c22092b8a0182880401ab31fe301]
Former-commit-id: ac5a0a3df55e8cb63297ade8345806671ec519f8
2017-08-07 14:48:02 +01:00
Henrique Dias
d6ca579519 Support replace feature; close #188; multiple bug fixes on upload
Former-commit-id: 30b18e418111df00903965a7e4135441cd1767a1 [formerly 60393b5e1a291d1aab2313e7dd8cd2e1e203c03b] [formerly c372bc1355db36d2d7b0bc602977616ae52a141c [formerly 6ee846ef0e]]
Former-commit-id: c6c5aa9646c78fe86af74aac00cc1c7c307ad678 [formerly 6d3accd032458203105a474b0f8204d8206c2440]
Former-commit-id: ab35a8effb0b01406bef44328ef3c45f5ea1cbfd
2017-08-07 14:44:32 +01:00
Toyo
d74c9ae5af Update zh-cn.yaml (#189)
Former-commit-id: e501daa9bb525b5695584fa5b8b99daa9571e27d [formerly de35b99396f1d39fee20aca6250cc61aeec6bcbc] [formerly 55ca4eb602d5b54dbc123cbc9772ae451a7f5ca7 [formerly c911eacc7b]]
Former-commit-id: e9ff475525798ada59ffa004ad6da46d34daa0b7 [formerly 893dce937e9fe40d314e3ed22ba0686d72e6b5aa]
Former-commit-id: 59093c53423038536ed5a7c695ce42d139ffe978
2017-08-05 10:25:33 +01:00
Henrique Dias
2f60562143 Add navigation links on users pages; close #187
Former-commit-id: 8e85fdb8938f3cb45561f0f7564f1877f8b7c1c1 [formerly 311658b86cd8c4304dfddaf04d89b06072b89245] [formerly da85ccd4c26d5aa9632884d0cc65ae259c5beeea [formerly a117f83256]]
Former-commit-id: 218b91f816d58ce0cc01cd2c7b590ac7ea5e589e [formerly f995eb799754c36567760aca8ce2455d33f6caa7]
Former-commit-id: f848927433956edda8a12083b8a53ce62d47f3ff
2017-08-05 10:06:12 +01:00
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
116 changed files with 16086 additions and 3923 deletions

View File

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

View File

@@ -7,15 +7,16 @@ RUN apk add --no-cache git
RUN go get ./...
WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager
RUN go install
RUN CGO_ENABLED=0 go build -a
RUN mv filemanager /go/bin/filemanager
FROM alpine:latest
COPY --from=0 /go/bin/filemanager /usr/local/bin/filemanager
FROM scratch
COPY --from=0 /go/bin/filemanager /filemanager
VOLUME /srv
EXPOSE 80
COPY Docker.json /etc/config.json
COPY Docker.json /config.json
ENTRYPOINT ["/usr/local/bin/filemanager"]
CMD ["--config", "/etc/config.json"]
ENTRYPOINT ["/filemanager"]
CMD ["--config", "/config.json"]

View File

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

View File

@@ -5,10 +5,14 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="base" content="{{ .BaseURL }}">
<meta name="staticgen" content="{{ .StaticGen }}">
<meta name="noauth" content="{{ .NoAuth }}">
<meta name="version" content="{{ .Version }}">
<meta name="recaptcha" content="{{ .ReCaptchaKey }}">
<title>File Manager</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="/static/img/icons/favicon.ico"><![endif]-->
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
<!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
<meta name="theme-color" content="#2979ff">
@@ -22,13 +26,17 @@
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<script>CSS = "{{ .CSS }}"</script>
{{ if .ReCaptcha -}}
<script src='https://www.google.com/recaptcha/api.js?render=explicit'></script>
{{ end }}
<% for (var chunk of webpack.chunks) {
for (var file of chunk.files) {
if (file.match(/\.(js|css)$/)) { %>
<link rel="<%= 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>{{ .JavaScript }}</script>
<style>
#loading {
position: fixed;

View File

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

View File

@@ -1,177 +0,0 @@
<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>
<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.variable">
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
</p>
</template>
<p><input type="submit" value="Save"></p>
</form>
<form @submit="saveCommands">
<h2>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>
<template v-for="command in commands">
<h3>{{ capitalize(command.name) }}</h3>
<textarea v-model.trim="command.value"></textarea>
</template>
<p><input type="submit" value="Save"></p>
</form>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import api from '@/utils/api'
export default {
name: 'settings',
data: function () {
return {
commands: [],
plugins: []
}
},
computed: {
...mapState([ 'user' ])
},
created () {
api.getCommands()
.then(commands => {
for (let key in commands) {
this.commands.push({
name: key,
value: commands[key].join('\n')
})
}
})
.catch(error => { this.showError(error) })
api.getPlugins()
.then(plugins => {
for (let key in plugins) {
this.plugins.push(this.parsePlugin(key, plugins[key]))
}
})
.catch(error => { this.showError(error) })
},
methods: {
...mapMutations([ 'showSuccess', 'showError' ]),
capitalize (name, where = '_') {
if (where === 'caps') where = /(?=[A-Z])/
let splitted = name.split(where)
name = ''
for (let i = 0; i < splitted.length; i++) {
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
}
return name.slice(0, -1)
},
saveCommands (event) {
event.preventDefault()
let commands = {}
for (let command of this.commands) {
let value = command.value.split('\n')
if (value.length === 1 && value[0] === '') {
value = []
}
commands[command.name] = value
}
api.updateCommands(commands)
.then(() => { this.showSuccess('Commands updated!') })
.catch(error => { this.showError(error) })
},
savePlugin (event) {
event.preventDefault()
let plugins = {}
for (let plugin of this.plugins) {
let p = {}
for (let field of plugin.fields) {
p[field.variable] = field.value
if (field.original === 'array') {
let val = field.value.split(' ')
if (val[0] === '') {
val.shift()
}
p[field.variable] = val
}
}
plugins[plugin.name] = p
}
console.log(plugins)
api.updatePlugins(plugins)
.then(() => { this.showSuccess('Plugins settings updated!') })
.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
}
}
}
</script>

View File

@@ -1,42 +1,35 @@
<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">
<button class="action"
v-for="action in plugin.header.visible"
v-if="action.if(pluginData, $route)"
@click="action.click($event, pluginData, $route)"
:aria-label="action.name"
:id="action.id"
:title="action.name"
:key="action.name">
<i class="material-icons">{{ action.icon }}</i>
<span>{{ action.name }}</span>
<template v-if="staticGen.length > 0">
<button v-show="showPublishButton" :aria-label="$t('buttons.publish')" :title="$t('buttons.publish')" class="action" id="publish-button">
<i class="material-icons">send</i>
</button>
</div>
</template>
<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>
<!-- Menu that shows on listing AND mobile when there are files selected -->
<div id="file-selection" v-if="isMobile && req.kind === 'listing'">
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
<share-button v-show="showRenameButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showMoveButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
@@ -46,34 +39,25 @@
<!-- This buttons are shown on a dropdown on mobile phones -->
<div id="dropdown" :class="{ active: showMore }">
<div v-if="!isListing || !isMobile">
<share-button v-show="showRenameButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showMoveButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
<delete-button v-show="showDeleteButton"></delete-button>
</div>
<div v-for="plugin in plugins" :key="plugin.name">
<button class="action"
v-for="action in plugin.header.hidden"
v-if="action.if(pluginData, $route)"
@click="action.click($event, pluginData, $route)"
:id="action.id"
:aria-label="action.name"
:title="action.name"
:key="action.name">
<i class="material-icons">{{ action.icon }}</i>
<span>{{ action.name }}</span>
</button>
</div>
<template v-if="staticGen.length > 0">
<schedule-button v-show="showPublishButton"></schedule-button>
</template>
<switch-button v-show="showSwitchButton"></switch-button>
<download-button v-show="showCommonButton"></download-button>
<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>
@@ -91,8 +75,10 @@ import DownloadButton from './buttons/Download'
import SwitchButton from './buttons/SwitchView'
import MoveButton from './buttons/Move'
import CopyButton from './buttons/Copy'
import ScheduleButton from './buttons/Schedule'
import ShareButton from './buttons/Share'
import {mapGetters, mapState} from 'vuex'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {
@@ -101,12 +87,14 @@ export default {
Search,
InfoButton,
DeleteButton,
ShareButton,
RenameButton,
DownloadButton,
CopyButton,
UploadButton,
SwitchButton,
MoveButton
MoveButton,
ScheduleButton
},
data: function () {
return {
@@ -134,7 +122,7 @@ export default {
'loading',
'reload',
'multiple',
'plugins'
'staticGen'
]),
isMobile () {
return this.width <= 736
@@ -148,6 +136,9 @@ export default {
showSaveButton () {
return (this.req.kind === 'editor' && !this.loading)
},
showPublishButton () {
return (this.req.kind === 'editor' && !this.loading && this.user.allowPublish)
},
showSwitchButton () {
return this.req.kind === 'listing' && this.$route.name === 'Files' && !this.loading
},

View File

@@ -0,0 +1,22 @@
<template>
<select v-on:change="change" :value="selected">
<option value="en">{{ $t('languages.en') }}</option>
<option value="fr">{{ $t('languages.fr') }}</option>
<option value="pt">{{ $t('languages.pt') }}</option>
<option value="ja">{{ $t('languages.ja') }}</option>
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
<option value="zh-tw">{{ $t('languages.zhTW') }}</option>
</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,53 +0,0 @@
<template>
<div>
<site-header></site-header>
<sidebar></sidebar>
<main>
<router-view v-on:css-updated="updateCSS"></router-view>
</main>
<prompts></prompts>
</div>
</template>
<script>
import Search from './Search'
import Sidebar from './Sidebar'
import Prompts from './prompts/Prompts'
import SiteHeader from './Header'
export default {
name: 'main',
components: {
Search,
Sidebar,
SiteHeader,
Prompts
},
watch: {
'$route': function () {
this.$store.commit('resetSelected')
this.$store.commit('multiple', false)
if (this.$store.state.show !== 'success') this.$store.commit('closeHovers')
}
},
mounted () {
this.updateCSS()
},
methods: {
updateCSS () {
let css = this.$store.state.user.css
let style = document.querySelector('style[title="user-css"]')
if (style !== undefined && style !== null) {
style.parentElement.removeChild(style)
}
style = document.createElement('style')
style.title = 'user-css'
style.type = 'text/css'
style.appendChild(document.createTextNode(css))
document.head.appendChild(style)
}
}
}
</script>

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,24 +11,70 @@
ref="input"
:autofocus="active"
v-model.trim="value"
aria-label="Write here to search"
:aria-label="$t('search.writeToSearch')"
:placeholder="placeholder">
</div>
<div id="result">
<div>
<span v-if="search.length === 0 && commands.length === 0">{{ text }}</span>
<template v-if="search.length === 0 && commands.length === 0">
<p>{{ text }}</p>
<template v-if="value.length === 0">
<div class="boxes">
<h3>{{ $t('search.types') }}</h3>
<div>
<div tabindex="0"
role="button"
@click="init('type:image')"
:aria-label="$t('search.images')">
<i class="material-icons">insert_photo</i>
<p>{{ $t('search.images') }}</p>
</div>
<div tabindex="0"
role="button"
@click="init('type:audio')"
:aria-label="$t('search.music')">
<i class="material-icons">volume_up</i>
<p>{{ $t('search.music') }}</p>
</div>
<div tabindex="0"
role="button"
@click="init('type:video')"
:aria-label="$t('search.video')">
<i class="material-icons">movie</i>
<p>{{ $t('search.video') }}</p>
</div>
<div tabindex="0"
role="button"
@click="init('type:pdf')"
:aria-label="$t('search.pdf')">
<i class="material-icons">picture_as_pdf</i>
<p>{{ $t('search.pdf') }}</p>
</div>
</div>
</div>
</template>
</template>
<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>
<ul v-else-if="commands.length > 0">
<li v-for="c in commands">{{ c }}</li>
</ul>
<pre v-else-if="commands.length > 0">
<template v-for="c in commands">{{ c }}</template>
</pre>
</div>
<p><i class="material-icons spin">autorenew</i></p>
<p id="renew"><i class="material-icons spin">autorenew</i></p>
</div>
</div>
</template>
@@ -36,7 +82,7 @@
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'search',
@@ -62,6 +108,8 @@ export default {
this.$store.commit('setReload', true)
}
document.body.style.overflow = 'auto'
this.reset()
this.$refs.input.blur()
}
@@ -70,6 +118,7 @@ export default {
if (val === 'search') {
this.reload = false
this.$refs.input.focus()
document.body.style.overflow = 'hidden'
}
}
},
@@ -78,10 +127,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.
@@ -92,16 +141,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')
}
}
},
@@ -119,19 +168,19 @@ export default {
},
methods: {
// Sets the search to active.
open: function (event) {
open (event) {
this.$store.commit('showHover', 'search')
},
// Closes the search and prevents the event
// of propagating so it doesn't trigger the
// click event on #search.
close: function (event) {
close (event) {
event.stopPropagation()
event.preventDefault()
this.$store.commit('closeHovers')
},
// Checks if the current input is a supported command.
supported: function () {
supported () {
let pieces = this.value.split(' ')
for (let i = 0; i < this.user.commands.length; i++) {
@@ -142,11 +191,24 @@ export default {
return false
},
// Initializes the search with a default value.
init (string) {
this.value = string + ' '
this.$refs.input.focus()
},
// Resets the search box value.
reset () {
this.value = ''
this.active = false
this.ongoing = false
this.search = []
this.commands = []
},
// When the user presses a key, if it is ESC
// then it will close the search box. Otherwise,
// it will set the search box to active and clean
// the search results, as well as commands'.
keyup: function (event) {
keyup (event) {
if (event.keyCode === 27) {
this.close(event)
return
@@ -156,7 +218,7 @@ export default {
this.commands.length = 0
},
// Submits the input to the server and sets ongoing to true.
submit: function (event) {
submit (event) {
this.ongoing = true
let path = this.$route.path
@@ -184,10 +246,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,45 +1,66 @@
<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>
<div v-for="plugin in plugins" :key="plugin.name">
<button v-for="action in plugin.sidebar" @click="action.click($event, pluginData, $route)" :aria-label="action.name" :title="action.name" :key="action.name" class="action">
<i class="material-icons">{{ action.icon }}</i>
<span>{{ action.name }}</span>
</button>
</div>
<div>
<router-link class="action" to="/settings" aria-label="Settings" title="Settings">
<i class="material-icons">settings_applications</i>
<span>Settings</span>
<div v-if="staticGen.length > 0">
<router-link to="/files/settings"
:aria-label="$t('sidebar.siteSettings')"
:title="$t('sidebar.siteSettings')"
class="action">
<i class="material-icons">settings</i>
<span>{{ $t('sidebar.siteSettings') }}</span>
</router-link>
<button @click="logout" class="action" id="logout" aria-label="Log out" title="Logout">
<template v-if="staticGen === 'hugo'">
<button class="action"
:aria-label="$t('sidebar.hugoNew')"
:title="$t('sidebar.hugoNew')"
v-if="user.allowNew"
@click="$store.commit('showHover', 'new-archetype')">
<i class="material-icons">merge_type</i>
<span>{{ $t('sidebar.hugoNew') }}</span>
</button>
</template>
<button class="action"
:aria-label="$t('sidebar.preview')"
:title="$t('sidebar.preview')"
@click="preview">
<i class="material-icons">remove_red_eye</i>
<span>{{ $t('sidebar.preview') }}</span>
</button>
</div>
<div v-if="!$store.state.noAuth">
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
<i class="material-icons">settings_applications</i>
<span>{{ $t('sidebar.settings') }}</span>
</router-link>
<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/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 rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a> v{{ version }}</span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
</p>
</nav>
</template>
@@ -47,31 +68,22 @@
<script>
import {mapState} from 'vuex'
import auth from '@/utils/auth'
import buttons from '@/utils/buttons'
import api from '@/utils/api'
export default {
name: 'sidebar',
data: function () {
return {
pluginData: {
api,
buttons,
'store': this.$store,
'router': this.$router
}
}
},
computed: {
...mapState(['user', 'plugins']),
...mapState(['user', 'staticGen', 'version']),
active () {
return this.$store.state.show === 'sidebar'
}
},
methods: {
help: function () {
help () {
this.$store.commit('showHover', 'help')
},
preview () {
window.open(this.$store.state.baseURL + '/preview/')
},
logout: auth.logout
}
}

View File

@@ -1,275 +0,0 @@
<template>
<div>
<form @submit="save" class="dashboard">
<h1 v-if="id === 0">New User</h1>
<h1 v-else>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>
<h2>Permissions</h2>
<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 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>
<input type="text" v-model.trim="commands">
<h2>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">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>
<p class="small"><strong>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>
</ul>
<textarea v-model.trim="rules"></textarea>
<h2>Custom Stylesheet</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">
</p>
</form>
<div v-if="$store.state.show === 'deleteUser'" class="prompt">
<h3>Delete User</h3>
<p>Are you sure you want to delete this user?</p>
<div>
<button @click="deleteUser" autofocus>Delete</button>
<button @click="closeHovers" class="cancel">Cancel</button>
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
import api from '@/utils/api'
export default {
name: 'user',
data: () => {
return {
id: 0,
admin: false,
allowNew: false,
allowEdit: false,
allowCommands: false,
permissions: {},
password: '',
username: '',
filesystem: '',
rules: '',
css: '',
commands: ''
}
},
computed: {
passwordPlaceholder () {
if (this.$route.path === '/users/new') return ''
return '(leave blank to avoid changes)'
}
},
created () {
this.fetchData()
},
watch: {
'$route': 'fetchData',
admin: function () {
if (!this.admin) return
this.allowCommands = true
this.allowEdit = true
this.allowNew = true
for (let key in this.permissions) {
this.permissions[key] = true
}
}
},
methods: {
...mapMutations(['closeHovers']),
fetchData () {
let user = this.$route.params[0]
if (this.$route.path === '/users/new') {
user = 'base'
}
api.getUser(user).then(user => {
this.id = user.ID
this.admin = user.admin
this.allowCommands = user.allowCommands
this.allowNew = user.allowNew
this.allowEdit = user.allowEdit
this.filesystem = user.filesystem
this.username = user.username
this.commands = user.commands.join(' ')
this.css = user.css
this.permissions = user.permissions
for (let rule of user.rules) {
if (rule.allow) {
this.rules += 'allow '
} else {
this.rules += 'disallow '
}
if (rule.regex) {
this.rules += 'regex ' + rule.regexp.raw
} else {
this.rules += rule.path
}
this.rules += '\n'
}
this.rules = this.rules.trim()
}).catch(() => {
this.$router.push({ path: '/users/new' })
})
},
capitalize (name) {
let splitted = name.split(/(?=[A-Z])/)
name = ''
for (let i = 0; i < splitted.length; i++) {
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
}
return name.slice(0, -1)
},
reset () {
this.id = 0
this.admin = false
this.allowNew = false
this.allowEdit = false
this.permissins = {}
this.allowCommands = false
this.password = ''
this.username = ''
this.filesystem = ''
this.rules = ''
this.css = ''
this.commands = ''
},
deletePrompt (event) {
this.$store.commit('showHover', 'deleteUser')
},
deleteUser (event) {
event.preventDefault()
api.deleteUser(this.id).then(location => {
this.$router.push({ path: '/users' })
this.$store.commit('showSuccess', 'User deleted!')
}).catch(e => {
this.$store.commit('showError', e)
})
},
save (event) {
event.preventDefault()
let user = this.parseForm()
if (this.$route.path === '/users/new') {
api.newUser(user).then(location => {
this.$router.push({ path: location })
this.$store.commit('showSuccess', 'User created!')
}).catch(e => {
this.$store.commit('showError', e)
})
return
}
api.updateUser(user).then(location => {
this.$store.commit('showSuccess', 'User updated!')
}).catch(e => {
this.$store.commit('showError', e)
})
},
parseForm () {
let user = {
ID: this.id,
username: this.username,
password: this.password,
filesystem: this.filesystem,
admin: this.admin,
allowCommands: this.allowCommands,
allowNew: this.allowNew,
allowEdit: this.allowEdit,
permissions: this.permissions,
css: this.css,
commands: this.commands.split(' '),
rules: []
}
let rules = this.rules.split('\n')
for (let rawRule of rules) {
let rule = {
allow: true,
path: '',
regex: false,
regexp: {
raw: ''
}
}
rawRule = rawRule.split(' ')
// Skip a malformed rule
if (rawRule.length < 2) {
continue
}
// Skip a malformed rule
if (rawRule[0] !== 'allow' && rawRule[0] !== 'disallow') {
continue
}
rule.allow = (rawRule[0] === 'allow')
rawRule.shift()
if (rawRule[0] === 'regex') {
rule.regex = true
rawRule.shift()
rule.regexp.raw = rawRule.join(' ')
} else {
rule.path = rawRule.join(' ')
}
user.rules.push(rule)
}
return user
}
}
}
</script>
<style>
</style>

View File

@@ -1,42 +0,0 @@
<template>
<div class="dashboard">
<h1>Users <router-link to="/users/new"><button>New</button></router-link></h1>
<table>
<tr>
<th>Username</th>
<th>Admin</th>
<th>Scope</th>
<th></th>
</tr>
<tr v-for="user in users">
<td>{{ user.username }}</td>
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
<td>{{ user.filesystem }}</td>
<td><router-link :to="'/users/' + user.ID"><i class="material-icons">mode_edit</i></router-link></td>
</tr>
</table>
</div>
</template>
<script>
import api from '@/utils/api'
export default {
name: 'users',
data: function () {
return {
users: []
}
},
created () {
api.getUsers().then(users => {
this.users = users
}).catch(error => {
this.$store.commit('showError', error)
})
}
}
</script>

View File

@@ -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,14 +1,14 @@
<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>
<script>
import {mapGetters, mapState} from 'vuex'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'download-button',

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

@@ -0,0 +1,21 @@
<template>
<button @click="show"
:aria-label="$t('buttons.schedule')"
:title="$t('buttons.schedule')"
id="schedule-button"
class="action">
<i class="material-icons">alarm</i>
<span>{{ $t('buttons.schedule') }}</span>
</button>
</template>
<script>
export default {
name: 'schedule-button',
methods: {
show: function (event) {
this.$store.commit('showHover', 'schedule')
}
}
}
</script>

View File

@@ -0,0 +1,17 @@
<template>
<button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action">
<i class="material-icons">share</i>
<span>{{ $t('buttons.share') }}</span>
</button>
</template>
<script>
export default {
name: 'share-button',
methods: {
show (event) {
this.$store.commit('showHover', 'share')
}
}
}
</script>

View File

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

View File

@@ -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,23 +1,23 @@
<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>
<script>
import { mapState } from 'vuex'
import CodeMirror from '@/utils/codemirror'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {
name: 'editor',
computed: {
...mapState(['req']),
...mapState(['req', 'schedule']),
hasMetadata: function () {
return (this.req.metadata !== undefined && this.req.metadata !== null)
}
@@ -32,10 +32,20 @@ export default {
created () {
window.addEventListener('keydown', this.keyEvent)
document.getElementById('save-button').addEventListener('click', this.save)
let publish = document.getElementById('publish-button')
if (publish !== null) {
publish.addEventListener('click', this.publish)
}
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
document.getElementById('save-button').removeEventListener('click', this.save)
let publish = document.getElementById('publish-button')
if (publish !== null) {
publish.removeEventListener('click', this.publish)
}
},
mounted: function () {
if (this.req.content === undefined || this.req.content === null) {
@@ -102,28 +112,32 @@ export default {
this.metalang = 'toml'
}
},
// Publishes the file.
publish (event) {
this.save(event, true)
},
// Saves the file.
save () {
buttons.loading('save')
save (event, regenerate = false) {
let button = regenerate ? 'publish' : 'save'
if (this.schedule !== '') button = 'schedule'
let content = this.content.getValue()
buttons.loading(button)
if (this.hasMetadata) {
content = this.metadata.getValue() + '\n\n' + content
}
api.put(this.$route.path, content)
api.put(this.$route.path, content, regenerate, this.schedule)
.then(() => {
buttons.done('save')
buttons.success(button)
this.$store.commit('setSchedule', '')
})
.catch(error => {
buttons.done('save')
this.$store.commit('showError', error)
buttons.done(button)
this.$showError(error)
this.$store.commit('setSchedule', '')
})
}
}
}
</script>
<style>
</style>

View File

@@ -2,37 +2,51 @@
<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"
@drop="drop"
:class="user.viewMode"
@dragenter="dragEnter"
@dragend="dragEnd">
<div>
<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: sizeSorted }" 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="{ active: modifiedSorted }" class="modified" @click="sort('modified')">
<span>Last modified</span>
<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"
@@ -47,7 +61,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"
@@ -62,12 +76,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>
@@ -77,14 +91,14 @@
import {mapState} from 'vuex'
import Item from './ListingItem'
import css from '@/utils/css'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {
name: 'listing',
components: { Item },
computed: {
...mapState(['req', 'selected']),
...mapState(['req', 'selected', 'user']),
nameSorted () {
return (this.req.sort === 'name')
},
@@ -196,17 +210,13 @@ export default {
if (this.$store.state.clipboard.key === 'x') {
api.move(items).then(() => {
this.$store.commit('setReload', true)
}).catch(error => {
this.$store.commit('showError', error)
})
}).catch(this.$showError)
return
}
api.copy(items).then(() => {
this.$store.commit('setReload', true)
}).catch(error => {
this.$store.commit('showError', error)
})
}).catch(this.$showError)
},
resizeEvent () {
// Update the columns size based on the window width.
@@ -215,7 +225,7 @@ export default {
if (columns === 0) columns = 1
items.style.width = `calc(${100 / columns}% - 1em)`
},
dragEnter: function (event) {
dragEnter (event) {
// When the user starts dragging an item, put every
// file on the listing with 50% opacity.
let items = document.getElementsByClassName('item')
@@ -224,61 +234,117 @@ export default {
file.style.opacity = 0.5
})
},
dragEnd: function (event) {
dragEnd (event) {
this.resetOpacity()
},
drop: function (event) {
event.preventDefault()
this.resetOpacity()
let dt = event.dataTransfer
let files = dt.files
let el = event.target
if (files.length <= 0) return
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
el = el.parentElement
}
}
if (files.length > 0) {
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
this.handleFiles(files, el.querySelector('.name').innerHTML + '/')
return
}
this.handleFiles(files, '')
} else {
this.resetOpacity()
let base = ''
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
base = el.querySelector('.name').innerHTML + '/'
}
if (base !== '') {
api.fetch(this.$route.path + base)
.then(req => {
this.checkConflict(files, req.items, base)
})
.catch(this.$showError)
return
}
this.checkConflict(files, this.req.items, base)
},
uploadInput: function (event) {
this.handleFiles(event.currentTarget.files, '')
checkConflict (files, items, base) {
if (typeof items === 'undefined' || items === null) {
items = []
}
let conflict = false
for (let i = 0; i < files.length; i++) {
let res = items.findIndex(function hasConflict (element) {
return (element.name === this)
}, files[i].name)
if (res >= 0) {
conflict = true
break
}
}
if (!conflict) {
this.handleFiles(files, base)
return
}
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
this.handleFiles(files, base, true)
}
})
},
resetOpacity: function () {
uploadInput (event) {
this.checkConflict(event.currentTarget.files, this.req.items, '')
},
resetOpacity () {
let items = document.getElementsByClassName('item')
Array.from(items).forEach(file => {
file.style.opacity = 1
})
},
handleFiles: function (files, base) {
this.resetOpacity()
handleFiles (files, base, overwrite = false) {
buttons.loading('upload')
let promises = []
let progress = new Array(files.length).fill(0)
for (let file of files) {
promises.push(api.post(this.$route.path + base + file.name, file))
let onupload = (id) => (event) => {
progress[id] = (event.loaded / event.total) * 100
let sum = 0
for (let i = 0; i < progress.length; i++) {
sum += progress[i]
}
this.$store.commit('setProgress', Math.ceil(sum / progress.length))
}
for (let i = 0; i < files.length; i++) {
let file = files[i]
promises.push(api.post(this.$route.path + base + file.name, file, overwrite, onupload(i)))
}
let finish = () => {
buttons.success('upload')
this.$store.commit('setProgress', 0)
}
Promise.all(promises)
.then(() => {
buttons.done('upload')
finish()
this.$store.commit('setReload', true)
})
.catch(error => {
buttons.done('upload')
this.$store.commit('showError', error)
finish()
this.$showError(error)
})
return false

View File

@@ -1,5 +1,7 @@
<template>
<div class="item"
role="button"
tabindex="0"
draggable="true"
@dragstart="dragStart"
@dragover="dragOver"
@@ -7,6 +9,8 @@
@click="click"
@dblclick="open"
@touchstart="touchstart"
:data-dir="isDir"
:aria-label="name"
:aria-selected="isSelected">
<div>
<i class="material-icons">{{ icon }}</i>
@@ -29,7 +33,7 @@
import { mapMutations, mapGetters, mapState } from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'item',
@@ -105,21 +109,24 @@ export default {
.then(() => {
this.$store.commit('setReload', true)
})
.catch(error => {
this.$store.commit('showError', error)
})
.catch(this.$showError)
},
click: function (event) {
if (this.selectedCount !== 0) event.preventDefault()
if (this.$store.state.selected.indexOf(this.index) === -1) {
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
} else {
if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index)
return
}
return false
if (event.shiftKey && this.selected.length === 1) {
let fi = (this.index > this.selected[0]) ? this.selected[0] : this.index
let la = (this.index > this.selected[0]) ? this.index : this.selected[0]
for (; fi <= la; fi++) this.addSelected(fi)
return
}
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
},
touchstart (event) {
setTimeout(() => {

View File

@@ -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,20 +11,24 @@
<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()">
<audio v-else-if="req.type == 'audio'" :src="raw()" controls></audio>
<video v-else-if="req.type == 'video'" :src="raw()" controls>
<audio v-else-if="req.type == 'audio'" :src="raw()" autoplay controls></audio>
<video v-else-if="req.type == 'video'" :src="raw()" autoplay controls>
Sorry, your browser doesn't support embedded videos,
but don't worry, you can <a :href="download()">download it</a>
and watch it with your favorite video player!
</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>
@@ -34,11 +38,11 @@
<script>
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 * as api from '@/utils/api'
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',
@@ -71,7 +75,7 @@ export default {
this.listing = req
this.updateLinks()
})
.catch(error => { console.log(error) })
.catch(this.$showError)
},
beforeDestroy () {
window.removeEventListener('keyup', this.key)

View File

@@ -1,13 +1,23 @@
<template>
<div class="prompt">
<h3>Copy</h3>
<p>Choose the place to copy your files:</p>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.copy') }}</h2>
</div>
<file-list @update:selected="val => dest = val"></file-list>
<div class="card-content">
<p>{{ $t('prompts.copyMessage') }}</p>
<file-list @update:selected="val => dest = val"></file-list>
</div>
<div>
<button class="ok" @click="copy">Copy</button>
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
</div>
</div>
</template>
@@ -15,7 +25,7 @@
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {
@@ -45,12 +55,12 @@ export default {
// Execute the promises.
api.copy(items)
.then(() => {
buttons.done('copy')
buttons.success('copy')
this.$router.push({ path: this.dest })
})
.catch(error => {
buttons.done('copy')
this.$store.commit('showError', error)
this.$showError(error)
})
}
}

View File

@@ -1,18 +1,25 @@
<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>
<div>
<button @click="submit" autofocus>Delete</button>
<button @click="closeHovers" class="cancel">Cancel</button>
<div class="card floating">
<div class="card-content">
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
</div>
<div class="card-action">
<button @click="$store.commit('closeHovers')"
class="flat cancel"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
class="flat"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
</div>
</div>
</template>
<script>
import {mapGetters, mapMutations, mapState} from 'vuex'
import api from '@/utils/api'
import { remove } from '@/utils/api'
import url from '@/utils/url'
import buttons from '@/utils/buttons'
@@ -31,14 +38,14 @@ export default {
// If we are not on a listing, delete the current
// opened file.
if (this.req.kind !== 'listing') {
api.delete(this.$route.path)
remove(this.$route.path)
.then(() => {
buttons.done('delete')
buttons.success('delete')
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
})
.catch(error => {
buttons.done('delete')
this.$store.commit('showError', error)
this.$showError(error)
})
return
@@ -54,18 +61,18 @@ export default {
let promises = []
for (let index of this.selected) {
promises.push(api.delete(this.req.items[index].url))
promises.push(remove(this.req.items[index].url))
}
Promise.all(promises)
.then(() => {
buttons.done('delete')
buttons.success('delete')
this.$store.commit('setReload', true)
})
.catch(error => {
buttons.done('delete')
this.$store.commit('setReload', true)
this.$store.commit('showError', error)
this.$showError(error)
})
}
}

View File

@@ -1,18 +1,24 @@
<template>
<div class="prompt" id="download">
<h3>Download files</h3>
<p>Choose the format you want to download.</p>
<button @click="download('zip')" autofocus>zip</button>
<button @click="download('tar')" autofocus>tar</button>
<button @click="download('targz')" autofocus>tar.gz</button>
<button @click="download('tarbz2')" autofocus>tar.bz2</button>
<button @click="download('tarxz')" autofocus>tar.xz</button>
<div class="card floating" id="download">
<div class="card-title">
<h2>{{ $t('prompts.download') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.downloadMessage') }}</p>
<button class="block cancel" @click="download('zip')" autofocus>zip</button>
<button class="block cancel" @click="download('tar')" autofocus>tar</button>
<button class="block cancel" @click="download('targz')" autofocus>tar.gz</button>
<button class="block cancel" @click="download('tarbz2')" autofocus>tar.bz2</button>
<button class="block cancel" @click="download('tarxz')" autofocus>tar.xz</button>
</div>
</div>
</template>
<script>
import {mapGetters, mapState} from 'vuex'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'download',

View File

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

View File

@@ -4,19 +4,22 @@
<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>
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'file-list',
@@ -50,7 +53,7 @@ export default {
// so we fetch the data from the previous directory.
api.fetch(url.removeLastDir(this.$route.path))
.then(this.fillOptions)
.catch(this.showError)
.catch(this.$showError)
},
methods: {
fillOptions (req) {
@@ -93,7 +96,7 @@ export default {
api.fetch(uri)
.then(this.fillOptions)
.catch(this.showError)
.catch(this.$showError)
},
touchstart (event) {
let url = event.currentTarget.dataset.url

View File

@@ -1,26 +1,29 @@
<template>
<div class="prompt help">
<h3>Help</h3>
<div class="card floating help">
<div class="card-title">
<h2>{{ $t('help.help') }}</h2>
</div>
<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>
<div class="card-content">
<ul>
<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>
<p>Not available yet</p>
<ul>
<li><strong>Alt + Click</strong> - select a group of files</li>
</ul>
<div>
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
<div class="card-action">
<button type="submit"
@click="$store.commit('closeHovers')"
class="flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div>
</div>
</template>

View File

@@ -1,27 +1,35 @@
<template>
<div class="prompt">
<h3>File Information</h3>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.fileInfo') }}</h2>
</div>
<p v-show="selected.length > 1">{{ selected.length }} files selected.</p>
<div class="card-content">
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
<p v-show="selected.length < 2"><strong>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-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
<p v-if="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>
</section>
<template v-if="dir() && selected.length === 0">
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
</template>
<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>
</section>
<template v-if="!dir()">
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
</template>
</div>
<div>
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
<div class="card-action">
<button type="submit"
@click="$store.commit('closeHovers')"
class="flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
</div>
</div>
</template>
@@ -30,7 +38,7 @@
import {mapState, mapGetters} from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'info',
@@ -107,7 +115,7 @@ export default {
api.checksum(link, hash)
.then((hash) => { event.target.innerHTML = hash })
.catch(error => { this.$store.commit('showError', error) })
.catch(this.$showError)
}
}
}

View File

@@ -1,13 +1,22 @@
<template>
<div class="prompt">
<h3>Move</h3>
<p>Choose new house for your file(s)/folder(s):</p>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.move') }}</h2>
</div>
<file-list @update:selected="val => dest = val"></file-list>
<div class="card-content">
<file-list @update:selected="val => dest = val"></file-list>
</div>
<div>
<button class="ok" @click="move">Move</button>
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
<div class="card-action">
<button class="flat cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="move"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
</div>
</div>
</template>
@@ -15,7 +24,7 @@
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {
@@ -45,12 +54,12 @@ export default {
// Execute the promises.
api.move(items)
.then(() => {
buttons.done('move')
buttons.success('move')
this.$router.push({ path: this.dest })
})
.catch(error => {
buttons.done('move')
this.$store.commit('showError', error)
this.$showError(error)
})
event.preventDefault()

View File

@@ -0,0 +1,72 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newFile') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newArchetype') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
</div>
<div class="card-action">
<button class="flat cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
</div>
</div>
</template>
<script>
import { removePrefix } from '@/utils/api'
export default {
name: 'new-archetype',
data: function () {
return {
name: '',
archetype: 'default'
}
},
methods: {
submit: function (event) {
event.preventDefault()
this.$store.commit('closeHovers')
this.new('/' + this.name, this.archetype)
.then((url) => {
this.$router.push({ path: url })
})
.catch(this.$showError)
},
new (url, type) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
if (!this.$store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
request.setRequestHeader('Archetype', encodeURIComponent(type))
request.onload = () => {
if (request.status === 200) {
resolve(request.getResponseHeader('Location'))
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
}
}
</script>

View File

@@ -1,18 +1,30 @@
<template>
<div class="prompt">
<h3>New directory</h3>
<p>Write the name of the new directory.</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>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newDir') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newDirMessage') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
</div>
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
@click="submit">{{ $t('buttons.create') }}</button>
</div>
</div>
</template>
<script>
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'new-dir',
@@ -37,7 +49,7 @@ export default {
api.post(uri)
.then(() => { this.$router.push({ path: uri }) })
.catch(error => { this.$store.commit('showError', error) })
.catch(this.$showError)
// Close the prompt
this.$store.commit('closeHovers')

View File

@@ -1,18 +1,30 @@
<template>
<div class="prompt">
<h3>New file</h3>
<p>Write the name of the new file.</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>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newFile') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newFileMessage') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
</div>
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
</div>
</div>
</template>
<script>
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'new-file',
@@ -38,7 +50,7 @@ export default {
// Create the new file.
api.post(uri)
.then(() => { this.$router.push({ path: uri }) })
.catch(error => { this.$store.commit('showError', error) })
.catch(this.$showError)
// Close the prompt.
this.$store.commit('closeHovers')

View File

@@ -9,29 +9,10 @@
<info v-else-if="showInfo"></info>
<move v-else-if="showMove"></move>
<copy v-else-if="showCopy"></copy>
<error v-else-if="showError"></error>
<success v-else-if="showSuccess"></success>
<template v-for="plugin in plugins">
<form class="prompt"
v-for="prompt in plugin.prompts"
:key="prompt.name"
v-if="show === prompt.name"
@submit="prompt.submit($event, pluginData, $route)">
<h3>{{ prompt.title }}</h3>
<p>{{ prompt.description }}</p>
<input v-for="input in prompt.inputs"
:key="input.name"
:type="input.type"
: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>
</div>
</form>
</template>
<replace v-else-if="showReplace"></replace>
<schedule v-else-if="show === 'schedule'"></schedule>
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
<share v-else-if="show === 'share'"></share>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</template>
@@ -44,28 +25,32 @@ import Rename from './Rename'
import Download from './Download'
import Move from './Move'
import Copy from './Copy'
import Error from './Error'
import Success from './Success'
import NewFile from './NewFile'
import NewDir from './NewDir'
import NewArchetype from './NewArchetype'
import Replace from './Replace'
import Schedule from './Schedule'
import Share from './Share'
import { mapState } from 'vuex'
import buttons from '@/utils/buttons'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'prompts',
components: {
Info,
Delete,
NewArchetype,
Schedule,
Rename,
Error,
Download,
Success,
Move,
Copy,
Share,
NewFile,
NewDir,
Help
Help,
Replace
},
data: function () {
return {
@@ -79,8 +64,6 @@ export default {
},
computed: {
...mapState(['show', 'plugins']),
showError: function () { return this.show === 'error' },
showSuccess: function () { return this.show === 'success' },
showInfo: function () { return this.show === 'info' },
showHelp: function () { return this.show === 'help' },
showDelete: function () { return this.show === 'delete' },
@@ -90,6 +73,7 @@ export default {
showNewFile: function () { return this.show === 'newFile' },
showNewDir: function () { return this.show === 'newDir' },
showDownload: function () { return this.show === 'download' },
showReplace: function () { return this.show === 'replace' },
showOverlay: function () {
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
}

View File

@@ -1,11 +1,24 @@
<template>
<div class="prompt">
<h3>Rename</h3>
<p>Insert a new name for <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>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.rename') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
</div>
<div class="card-action">
<button class="cancel flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
class="flat"
type="submit"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
</div>
</div>
</template>
@@ -13,7 +26,7 @@
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'rename',
@@ -61,7 +74,7 @@ export default {
}
this.$store.commit('setReload', true)
}).catch(error => {
this.$store.commit('showError', error)
this.$showError(error)
})
this.$store.commit('closeHovers')

View File

@@ -0,0 +1,31 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p>
</div>
<div class="card-action">
<button class="flat cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="flat"
@click="showConfirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'replace',
computed: mapState(['showConfirm'])
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.schedule') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.scheduleMessage') }}</p>
<input autofocus type="datetime-local" v-model="date">
</div>
<div class="card-action">
<button class="cancel flat"
@click="close"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="falt"
@click="submit"
:aria-label="$t('buttons.schedule')"
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
</div>
</div>
</template>
<script>
export default {
name: 'schedule',
data: function () {
return {
date: ''
}
},
methods: {
close () {
this.$store.commit('closeHovers')
},
submit: function (event) {
event.preventDefault()
if (this.date === '') return
this.close()
this.$store.commit('setSchedule', this.date)
document.getElementById('save-button').click()
}
}
}
</script>

View File

@@ -0,0 +1,162 @@
<template>
<div class="card floating" id="share">
<div class="card-title">
<h2>{{ $t('buttons.share') }}</h2>
</div>
<div class="card-content">
<ul>
<li v-if="!hasPermanent">
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
</li>
<li v-for="link in links" :key="link.hash">
<a :href="buildLink(link.hash)" target="_blank">
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
<template v-else>{{ $t('permanent') }}</template>
</a>
<button class="action"
@click="deleteLink($event, link)"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
<button class="action copy-clipboard"
:data-clipboard-text="buildLink(link.hash)"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
</li>
<li>
<input autofocus
type="number"
max="2147483647"
min="0"
@keyup.enter="submit"
v-model.trim="time">
<select v-model="unit" :aria-label="$t('time.unit')">
<option value="seconds">{{ $t('time.seconds') }}</option>
<option value="minutes">{{ $t('time.minutes') }}</option>
<option value="hours">{{ $t('time.hours') }}</option>
<option value="days">{{ $t('time.days') }}</option>
</select>
<button class="action"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
</li>
</ul>
</div>
<div class="card-action">
<button class="flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { getShare, deleteShare, share } from '@/utils/api'
import moment from 'moment'
import Clipboard from 'clipboard'
export default {
name: 'share',
data: function () {
return {
time: '',
unit: 'hours',
hasPermanent: false,
links: [],
clip: null
}
},
computed: {
...mapState([ 'baseURL', 'req', 'selected', 'selectedCount' ]),
url () {
// Get the current name of the file we are editing.
if (this.req.kind !== 'listing') {
return this.$route.path
}
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return
}
return this.req.items[this.selected[0]].url
}
},
beforeMount () {
getShare(this.url)
.then(links => {
this.links = links
this.sort()
for (let link of this.links) {
if (!link.expires) {
this.hasPermanent = true
break
}
}
})
.catch(error => {
if (error === 404) return
this.$showError(error)
})
},
mounted () {
this.clip = new Clipboard('.copy-clipboard')
this.clip.on('success', (e) => {
this.$showSuccess(this.$t('success.linkCopied'))
})
},
beforeDestroy () {
this.clip.destroy()
},
methods: {
submit: function (event) {
if (!this.time) return
share(this.url, this.time, this.unit)
.then(result => { this.links.push(result); this.sort() })
.catch(this.$showError)
},
getPermalink (event) {
share(this.url)
.then(result => {
this.links.push(result)
this.sort()
this.hasPermanent = true
})
.catch(this.$showError)
},
deleteLink (event, link) {
event.preventDefault()
deleteShare(link.hash)
.then(() => {
if (!link.expires) this.hasPermanent = false
this.links = this.links.filter(item => item.hash !== link.hash)
})
.catch(this.$showError)
},
humanTime (time) {
return moment(time).fromNow()
},
buildLink (hash) {
return `${window.location.origin}${this.baseURL}/share/${hash}`
},
sort () {
this.links = this.links.sort((a, b) => {
if (!a.expires) return -1
if (!b.expires) return 1
return new Date(a.expireDate) - new Date(b.expireDate)
})
}
}
}
</script>

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,7 @@ header>div div {
position: relative;
}
header > div:last-child div {
header>div:last-child div {
display: flex;
}
@@ -122,17 +122,18 @@ header .search-button {
#search input {
width: 100%;
border: 0;
outline: 0;
background-color: transparent;
line-height: 0;
padding: 0;
}
#search #result {
visibility: visible;
max-height: none;
background-color: #fff;
background-color: #f8f8f8;
text-align: left;
color: #ccc;
padding: 0;
color: rgba(0, 0, 0, 0.6);
height: 0;
transition: .1s ease height, .1s ease padding;
overflow-x: hidden;
@@ -140,6 +141,10 @@ header .search-button {
z-index: 1;
}
#search #result>div>*:first-child {
margin-top: 0;
}
#search.active #result {
padding: .5em;
height: calc(100% - 4em);
@@ -155,15 +160,12 @@ header .search-button {
margin-bottom: .5em;
}
#search #result div {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
#search #result>div {
max-width: 45em;
margin: 0 auto;
}
#search #result p {
#search #result #renew {
width: 100%;
text-align: center;
display: none;
@@ -171,17 +173,30 @@ header .search-button {
max-width: none;
}
#search.ongoing #result p {
#search.ongoing #result #renew {
display: block;
}
#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);
}
@@ -199,3 +214,47 @@ header .search-button {
#search:-ms-input-placeholder {
color: rgba(255, 255, 255, .5);
}
#search .boxes {
border: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
background: #fff;
margin: 1em 0;
}
#search .boxes h3 {
margin: 0;
font-weight: 500;
font-size: 1em;
color: #212121;
padding: .5em;
}
#search .boxes>div {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-right: -1em;
margin-bottom: -1em;
}
#search .boxes>div>div {
background: #2196F3;
color: #fff;
text-align: center;
width: 10em;
padding: 1em;
cursor: pointer;
margin-bottom: 1em;
margin-right: 1em;
flex-grow: 1;
}
#search .boxes p {
margin: 1em 0 0;
}
#search .boxes i {
color: #fff !important;
font-size: 3.5em;
}

View File

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

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

@@ -0,0 +1,76 @@
#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.recaptcha form {
min-width: 304px;
}
#login #recaptcha {
margin: .5em 0 0;
}
#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

@@ -61,7 +61,7 @@
background: #fff;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
width: 95%;
max-width: 18em;
max-width: 20em;
}
#file-selection .action {
border-radius: 50%;

View File

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

View File

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

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

@@ -0,0 +1,200 @@
permanent: Permanent
buttons:
cancel: Cancel
close: Close
copy: Copy
copyFile: Copy file
copyToClipboard: Copy to clipboard
create: Create
delete: Delete
download: Download
info: Info
more: More
move: Move
moveFile: Move file
new: New
next: Next
ok: OK
replace: Replace
previous: Previous
rename: Rename
reportIssue: Report Issue
save: Save
search: Search
select: Select
share: Share
publish: Publish
selectMultiple: Select multiple
schedule: Schedule
switchView: Switch view
toggleSidebar: Toggle sidebar
update: Update
upload: Upload
permalink: Get Permanent Link
success:
linkCopied: Link copied!
errors:
forbidden: You're not welcome here.
internal: Something really went wrong.
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
replace: Replace
replaceMessage: >
One of the files you're trying to upload is conflicting because of its name.
Do you wish to replace the existing one?
rename: Rename
renameMessage: Insert a new name for
show: Show
size: Size
schedule: Schedule
scheduleMessage: Pick a date and time to schedule the publication of this post.
newArchetype: Create a new post based on an archetype. Your file will be created on content folder.
settings:
admin: Admin
administrator: Administrator
allowCommands: Execute commands
allowEdit: Edit, rename and delete files or directories
allowNew: Create new files and directories
allowPublish: Publish new posts and pages
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
lockPassword: Prevent the user from changing the password
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.
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 wont be accessible
to the user. We support regex and paths relative to the users 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
settings: Settings
siteSettings: Site Settings
hugoNew: Hugo New
preview: Preview
search:
images: Images
music: Music
pdf: PDF
pressToExecute: Press enter to execute.
pressToSearch: Press enter to search.
search: Search...
searchOrCommand: Search or execute a command...
searchOrSupportedCommand: 'Search or use one of your supported commands:'
type: Type and press enter to search.
types: Types
video: Video
writeToSearch: Write here to search
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time:
unit: Time Unit
seconds: Seconds
minutes: Minutes
hours: Hours
days: Days

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

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

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

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

200
assets/src/i18n/ja.yaml Normal file
View File

@@ -0,0 +1,200 @@
permanent: 永久
buttons:
cancel: キャンセル
close: 閉じる
copy: コピー
copyFile: ファイルをコピー
copyToClipboard: クリップボードにコピー
create: 作成
delete: 削除
download: ダウンロード
info: 情報
more: More
move: 移動
moveFile: ファイルを移動
new: 新規
next:
ok: OK
replace: 置き換える
previous:
rename: 名前を変更
reportIssue: 問題を報告
save: 保存
search: 検索
select: 選択
share: シェア
publish: 発表
selectMultiple: 複数選択
schedule: スケジュール
switchView: 表示を切り替わる
toggleSidebar: サイドバーを表示する
update: 更新
upload: アップロード
permalink: 固定リンク
success:
linkCopied: リンクがコピーされました!
errors:
forbidden: アクセスが拒否されました。
internal: 内部エラーが発生しました。
notFound: リソースが見つからなりませんでした。
files:
folders: フォルダ
files: ファイル
body: 本文
clear: クリアー
closePreview: プレビューを閉じる
home: ホーム
lastModified: 最終変更
loading: ローディング...
lonely: ここには何もない...
metadata: メタデータ
multipleSelectionEnabled: 複数選択有効
name: 名前
size: サイズ
sortByName: 名前によるソート
sortBySize: サイズによるソート
sortByLastModified: 最終変更日付によるソート
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: ファイル個数
replace: 置き換える
replaceMessage: >
アップロードするファイルの中でかち合う名前が一つあります。
既存のファイルを置き換えりませんか。
rename: 名前を変更
renameMessage: 名前を変更しようファイルは:
show: 表示
size: サイズ
schedule: スケジュール
scheduleMessage: このポストの発表日付をスケジュールしてください。
newArchetype: ある元型に基づいて新しいポストを作成します。ファイルは コンテンツフォルダに作成されます。
settings:
admin: 管理者
administrator: 管理者
allowCommands: コマンドの実行
allowEdit: ファイルやディレクトリの編集、名前変更と削除
allowNew: ファイルとディレクトリの作成
allowPublish: ポストとぺーじの発表
avoidChanges: "(変更を避けるために空白にしてください)"
changePassword: パスワードを変更
commands: コマンド
commandsHelp: "\
ここで、名前付きイベントに実行するコマンドを設定することができます。\
一行にコマンド一つを入力してください。\
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\
環境変数 FILE はファイルのパスに割り当てられます。"
commandsUpdated: コマンドは更新されました!
customStylesheet: カスタムスタイルシ ート
examples:
globalSettings: グローバル設定
language: 言語
lockPassword: 新しいパスワードを変更に禁止
newPassword: 新しいパスワード
newPasswordConfirm: 新しいパスワードを確認します
newUser: 新しいユーザー
password: パスワード
passwordUpdated: パスワードは更新されました!
permissions: 権限
permissionsHelp: "\
あなたはユーザーを管理者に設定し、または権限を個々に設定しできます。\
\"管理者\"を選択する場合、その他のすべての選択肢は自動的に設定されます。\
ユーザーの管理は管理者の権限として保留されました。"
profileSettings: プロファイル設定
ruleExample1: "\
各フォルダに名前はドットで始まるファイル(例えば、.git、.gitignore\
へのアクセスを制限します。"
ruleExample2: 範囲のルートパスに名前は Caddyfile のファイルへのアクセスを制限します。
rules: 規則
rulesHelp1: "\
ここに、あなたはこのユーザーの許可または拒否規則を設定できます。\
ブロックされたファイルはリストに表示されません、それではアクセスも制限されます。\
正規表現(regex)のサポートと範囲に相対のパスが提供されています。"
rulesHelp2: "\
一行に規則一つを入力してください、\
その間に規則はキーワード {0} や {1} で始める必要があります。\
そして正規表現を使う場合、{2} と入力し、表現やパスを入力してください。"
scope: 範囲
settingsUpdated: 設定は更新されました!
user: ユーザー
userCommands: ユーザーのコマンド
userCommandsHelp: "\
空白区切りの有効のコマンドのリストを指定してください。\
例:"
userCreated: ユーザーは作成されました!
userDeleted: ユーザーは削除されました!
userManagement: ユーザー管理
username: ユーザー名
users: ユーザー
userUpdated: ユーザーは更新されました!
sidebar:
help: ヘルプ
logout: ログアウト
myFiles: 私のファイル
newFile: 新しいファイルを作成
newFolder: 新しいフォルダを作成
settings: 設定
siteSettings: サイト設定
hugoNew: Hugo New
preview: プレビュー
search:
images: 画像
music: 音楽
pdf: PDF
pressToExecute: Enter を押して実行します。
pressToSearch: Enter を押して検索します。
search: 検索...
searchOrCommand: コマンドを検索または実行します。
searchOrSupportedCommand: サポートしているコマンドを検索または実行します:
type: キーワードを入力し、Enter を押して検索します。
types: 種類
video: ビデオ
writeToSearch: ここにキーワードを入力してください
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time:
unit: 時間単位
seconds:
minutes:
hours: 時間
days:

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

@@ -0,0 +1,203 @@
permanent: Permanente
buttons:
cancel: Cancelar
close: Fechar
copy: Copiar
copyFile: Copiar ficheiro
copyToClipboard: Copiar
create: Criar
delete: Eliminar
download: Descarregar
info: Info
more: Mais
move: Mover
moveFile: Mover ficheiro
new: Novo
next: Próximo
ok: OK
previous: Anterior
publish: Publicar
rename: Renomear
replace: Substituir
reportIssue: Reportar Erro
save: Guardar
share: Partilhar
schedule: Agendar
search: Pesquisar
select: Selecionar
selectMultiple: Selecionar múltiplos
switchView: Alterar modo de visão
toggleSidebar: Alternar barra lateral
update: Atualizar
upload: Enviar
permalink: Obter link permanente
success:
linkCopied: Link copiado!
errors:
forbidden: Tu não és bem-vindo aqui.
internal: Algo correu bastante mal.
notFound: Não conseguimos chegar a esta localização.
files:
body: Corpo
clear: Limpar
closePreview: Fechar pré-visualização
files: Ficheiros
folders: Pastas
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
sortByLastModified: Ordenar pela última modificação
sortByName: Ordenar pelo nome
sortBySize: Ordenar pelo tamanho
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
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
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:'
newArchetype: Criar um novo post baseado num "archetype". O seu ficheiro será criado
na pasta "content".
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
replace: Substituir
replaceMessage: >
Já existe um ficheiro com nome igual a um dos que está a tentar
enviar. Deseja substituir?
schedule: Agendar
scheduleMessage: Escolha uma data para publicar este post.
show: Mostrar
size: Tamanho
search:
images: Imagens
music: Música
pdf: PDF
pressToExecute: Prima enter para executar.
pressToSearch: Prima enter para pesquisar.
search: Pesquise...
searchOrCommand: Pesquise ou execute um comando...
searchOrSupportedCommand: 'Pesquise ou utilize um dos seus comandos:'
type: Escreva e prima enter para pesquisar.
types: Tipos
video: Vídeos
writeToSearch: Escreva aqui para pesquisar
settings:
admin: Admin
administrator: Administrador
allowCommands: Executar comandos
allowEdit: Editar, renomear e eliminar ficheiros ou pastas
allowNew: Criar novos ficheiros e pastas
allowPublish: Publicar novas páginas e conteúdos
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
lockPassword: Não permitir que o utilizador altere a palavra-passe
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.
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
hugoNew: Hugo New
logout: Sair
myFiles: Ficheiros
newFile: Novo ficheiro
newFolder: Nova pasta
preview: Pré-visualizar
settings: Configurações
siteSettings: Configurações do Site
time:
unit: Unidades de Tempo
seconds: Segundos
minutes: Minutos
hours: Horas
days: Dias

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

@@ -0,0 +1,198 @@
permanent: 永久
buttons:
cancel: 取消
close: 关闭
copy: 复制
copyFile: 复制文件
copyToClipboard: 复制到剪贴板
create: 创建
delete: 删除
download: 下载
info: 信息
more: 更多
move: 移动
moveFile: 移动文件
new:
next: 下一个
ok: 确定
replace: 替换
previous: 上一个
rename: 重命名
reportIssue: 报告问题
save: 保存
search: 搜索
select: 选择
share: 分享
publish: 发布
selectMultiple: 选择多个
schedule: 计划
switchView: 切换显示方式
toggleSidebar: 切换侧边栏
update: 更新
upload: 上传
permalink: 获取永久链接
success:
linkCopied: 链接已复制!
errors:
forbidden: 你被禁止访问。
internal: 内部出现麻烦了。
notFound: 找不到文件。
files:
folders: 文件夹
files: 文件
body: Body
clear: 清空
closePreview: 关闭预览
home: 主页
lastModified: 最后修改
loading: 加载中...
lonely: 这里没有任何文件...
metadata: 元数据
multipleSelectionEnabled: 多选模式已开启
name: 名称
size: 大小
sortByName: 按名称排序
sortBySize: 按大小排序
sortByLastModified: 按最后修改时间排序
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: 文件数
replace: 替换
replaceMessage: "\
您尝试上传的文件中有一个与现有文件的名称存在冲突。\
是否替换现有的同名文件?"
rename: 重命名
renameMessage: 请输入新名称,旧名称为:
show: 揭示
size: 大小
schedule: 计划
scheduleMessage: 请选择发布这篇帖子的日期。
newArchetype: 创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。
settings:
admin: 管理员
administrator: 管理员
allowCommands: 执行命令(Linux 代码)
allowEdit: 编辑、重命名或删除文件/目录
allowNew: 创建新文件和目录
allowPublish: 发布新的帖子与页面
avoidChanges: '(留空以避免更改)'
changePassword: 更改密码
commands: 命令(linux 代码)
commandsHelp: "\
在这里,您可以设置在指定事件下执行的命令,一行一条。\
若事件与文件相关,如“在保存文件前”,\
则文件的路径会被赋值给环境变量 \"FILE\"。"
commandsUpdated: 命令已更新!
customStylesheet: 自定义样式表
examples: 例子
globalSettings: 全局设置
language: 语言
lockPassword: 禁止用户修改密码
newPassword: 您的新密码
newPasswordConfirm: 重输一遍新密码
newUser: 新建用户
password: 密码
passwordUpdated: 密码已更新!
permissions: 权限
permissionsHelp: "\
您可以将该用户设置为管理员,也可以单独选择各项权限。\
如果选择了“管理员”,则其他的选项会被自动勾上,\
同时该用户可以管理其他用户。"
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: 新建文件夹
settings: 设置
siteSettings: 网站设置
hugoNew: Hugo New
preview: 预览
search:
images: 图像
music: 音乐
pdf: PDF
pressToExecute: 按回车键执行。
pressToSearch: 按回车键搜索。
search: 搜索...
searchOrCommand: 搜索或者执行命令(Linux 代码)...
searchOrSupportedCommand: 搜索或使用您可以使用的命令(一次只能执行一个命令)
type: 键入并按回车键进行搜索。
types: 类型
video: 视频
writeToSearch: 请输入要搜索的内容
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time:
unit: 时间单位
seconds:
minutes: 分钟
hours: 小时
days:

198
assets/src/i18n/zh-tw.yaml Normal file
View File

@@ -0,0 +1,198 @@
permanent: 永久
buttons:
cancel: 取消
close: 關閉
copy: 複製
copyFile: 複製檔案
copyToClipboard: 複製到剪貼簿
create: 建立
delete: 刪除
download: 下載
info: 資訊
more: 更多
move: 移動
moveFile: 移動檔案
new:
next: 下一個
ok: 確認
replace: 更換
previous: 上一個
rename: 重新命名
reportIssue: 報告問題
save: 儲存
search: 搜尋
select: 選擇
share: 分享
publish: 發佈
selectMultiple: 選擇多個
schedule: 計畫
switchView: 切換顯示方式
toggleSidebar: 切換側邊欄
update: 更新
upload: 上傳
permalink: 獲取永久連結
success:
linkCopied: 連結已複製!
errors:
forbidden: 你被禁止存取。
internal: 內部出現麻煩了。
notFound: 找不到檔案。
files:
folders: 資料夾
files: 檔案
body: Body
clear: 清空
closePreview: 關閉預覽
home: 主頁
lastModified: 最後修改
loading: 讀取中...
lonely: 這裡沒有任何檔案...
metadata: 詮釋資料
multipleSelectionEnabled: 多選模式已開啟
name: 名稱
size: 大小
sortByName: 按名稱排序
sortBySize: 按大小排序
sortByLastModified: 按最後修改時間排序
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: 檔案數
replace: 替換
replaceMessage: "\
您嘗試上傳的檔案中有一個與現有檔案的名稱存在衝突。\
是否取代現有的同名檔案?"
rename: 重新命名
renameMessage: 請輸入新名稱,舊名稱為:
show: 顯示
size: 大小
schedule: 計畫
scheduleMessage: 請選擇發佈這篇貼文的日期。
newArchetype: 建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。
settings:
admin: 管理員
administrator: 管理員
allowCommands: 執行命令
allowEdit: 編輯、重命名或刪除檔案/目錄
allowNew: 創建新檔案和目錄
allowPublish: 發佈新的貼文與頁面
avoidChanges: '(留空以避免更改)'
changePassword: 更改密碼
commands: 命令
commandsHelp: "\
在這裡,您可以設定在指定事件下執行的命令,一行一條。\
若事件與檔案相關,如“在保存檔案前”,\
則檔案的路徑會被賦值給環境變數 \"FILE\"。"
commandsUpdated: 命令已更新!
customStylesheet: 自定義樣式表
examples: 範例
globalSettings: 全域設定
language: 語言
lockPassword: 禁止使用者修改密碼
newPassword: 您的新密碼
newPasswordConfirm: 重輸一遍新密碼
newUser: 建立使用者
password: 密碼
passwordUpdated: 密碼已更新!
permissions: 權限
permissionsHelp: "\
您可以將該使用者設置為管理員,也可以單獨選擇各項權限。\
如果選擇了“管理員”,則其他的選項會被自動勾上,\
同時該使用者可以管理其他使用者。"
profileSettings: 設定檔設定
ruleExample1: "\
封鎖使用者存取所有資料夾下任何以 . 開頭的檔案\
(隱藏文件, 例如: .git, .gitignore)。"
ruleExample2: 封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。
rules: 規則
rulesHelp1: "\
您可以為該使用者製定一組黑名單或白名單式的規則,\
被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,\
支持相對於目錄範圍的路徑。"
rulesHelp2: "\
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
scope: 目錄範圍
settingsUpdated: 設定已更新!
user: 使用者
userCommands: 使用者命令
userCommandsHelp: "\
指定該使用者可以執行的命令,用空格分隔。\
例如:"
userCreated: 使用者已建立!
userDeleted: 使用者已刪除!
userManagement: 使用者管理
username: 使用者名稱
users: 使用者
userUpdated: 使用者已更新!
sidebar:
help: 幫助
logout: 登出
myFiles: 我的檔案
newFile: 建立檔案
newFolder: 建立資料夾
settings: 設定
siteSettings: 網站設定
hugoNew: Hugo New
preview: 預覽
search:
images: 影像
music: 音樂
pdf: PDF
pressToExecute: 按確定鍵執行。
pressToSearch: 按確定鍵搜尋。
search: 搜尋...
searchOrCommand: 搜尋或者執行命令...
searchOrSupportedCommand: 搜尋或使用您可以使用的命令(一次只能執行一個命令)
type: 輸入並按確定鍵進行搜尋。
types: 類型
video: 影片
writeToSearch: 請輸入要搜尋的內容
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time:
unit: 時間單位
seconds:
minutes: 分鐘
hours: 小時
days:

View File

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

View File

@@ -1,15 +1,16 @@
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/Settings/Users'
import User from '@/views/Settings/User'
import Settings from '@/views/Settings'
import GlobalSettings from '@/views/settings/Global'
import ProfileSettings from '@/views/settings/Profile'
import Error403 from '@/views/errors/403'
import Error404 from '@/views/errors/404'
import Error500 from '@/views/errors/500'
import auth from '@/utils/auth.js'
import store from '@/store'
@@ -25,24 +26,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
},
@@ -55,58 +50,61 @@ const router = new Router({
{
path: '/settings',
name: 'Settings',
component: Settings,
redirect: {
path: '/settings/profile'
}
},
{
path: '/settings/profile',
name: 'Profile Settings',
component: ProfileSettings
},
{
path: '/settings/global',
name: 'Global Settings',
component: GlobalSettings,
meta: {
requiresAdmin: true
}
},
children: [
{
path: '/settings/profile',
name: 'Profile Settings',
component: ProfileSettings
},
{
path: '/settings/global',
name: 'Global Settings',
component: GlobalSettings,
meta: {
requiresAdmin: true
}
},
{
path: '/settings/users',
name: 'Users',
component: Users,
meta: {
requiresAdmin: true
}
},
{
path: '/settings/users/*',
name: 'User',
component: User,
meta: {
requiresAdmin: true
}
}
]
},
{
path: '/403',
name: 'Forbidden',
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',
name: 'Users',
component: Users,
meta: {
requiresAdmin: true
}
},
{
path: '/users/',
path: '/files',
redirect: {
path: '/users'
}
},
{
path: '/users/*',
name: 'User',
component: User,
meta: {
requiresAdmin: true
path: '/files/'
}
},
{

View File

@@ -8,19 +8,30 @@ Vue.use(Vuex)
const state = {
user: {},
req: {},
plugins: window.plugins || [],
clipboard: {
key: '',
items: []
},
css: (() => {
let css = window.CSS
window.CSS = null
return css
})(),
recaptcha: document.querySelector('meta[name="recaptcha"]').getAttribute('content'),
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'),
version: document.querySelector('meta[name="version"]').getAttribute('content'),
jwt: '',
progress: 0,
schedule: '',
loading: false,
reload: false,
selected: [],
multiple: false,
show: null,
showMessage: null
showMessage: null,
showConfirm: null
}
export default new Vuex.Store({

View File

@@ -1,3 +1,6 @@
import i18n from '@/i18n'
import moment from 'moment'
const mutations = {
closeHovers: state => {
state.show = null
@@ -11,6 +14,7 @@ const mutations = {
state.show = value.prompt
state.showMessage = value.message
state.showConfirm = value.confirm
},
showError: (state, value) => {
state.show = 'error'
@@ -22,11 +26,18 @@ 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) => {
moment.locale(value.locale)
i18n.locale = value.locale
state.user = value
},
setCSS: (state, value) => (state.css = value),
setJWT: (state, value) => (state.jwt = value),
multiple: (state, value) => (state.multiple = value),
addSelected: (state, value) => (state.selected.push(value)),
addPlugin: (state, value) => {
state.plugins.push(value)
},
removeSelected: (state, value) => {
let i = state.selected.indexOf(value)
if (i === -1) return
@@ -35,8 +46,12 @@ const mutations = {
resetSelected: (state) => {
state.selected = []
},
listingDisplay: (state, value) => {
state.req.display = value
updateUser: (state, value) => {
if (typeof value !== 'object') return
for (let field in value) {
state.user[field] = value[field]
}
},
updateRequest: (state, value) => {
state.req = value
@@ -48,6 +63,12 @@ const mutations = {
resetClipboard: (state) => {
state.clipboard.key = ''
state.clipboard.items = []
},
setSchedule: (state, value) => {
state.schedule = value
},
setProgress: (state, value) => {
state.progress = value
}
}

View File

@@ -2,21 +2,23 @@ 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)
url = url.slice(6)
}
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url
return url
}
function fetch (url) {
export function fetch (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -24,10 +26,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,13 +35,13 @@ function fetch (url) {
})
}
function rm (url) {
export function remove (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
@@ -57,34 +56,51 @@ function rm (url) {
})
}
function post (url, content = '') {
export function post (url, content = '', overwrite = false, onupload) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (typeof onupload === 'function') {
request.upload.onprogress = onupload
}
if (overwrite) {
request.setRequestHeader('Action', `override`)
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
} else if (request.status === 409) {
reject(request.status)
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.onerror = (error) => {
reject(error)
}
request.send(content)
})
}
function put (url, content = '') {
export function put (url, content = '', publish = false, date = '') {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.setRequestHeader('Publish', publish)
if (date !== '') {
request.setRequestHeader('Schedule', date)
}
request.onload = () => {
if (request.status === 200) {
@@ -109,7 +125,7 @@ function moveCopy (items, copy = false) {
promises.push(new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PATCH', `${store.state.baseURL}/api/resource${from}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.setRequestHeader('Destination', to)
if (copy) {
@@ -132,21 +148,21 @@ 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) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/checksum${url}?algo=${algo}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
@@ -160,7 +176,7 @@ 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.host}${store.state.baseURL}/api/command${url}`
@@ -171,7 +187,7 @@ 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.host}${store.state.baseURL}/api/search${url}`
@@ -182,7 +198,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,11 +222,63 @@ 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)
if (!store.state.noAuth) 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)
if (!store.state.noAuth) 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)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -227,11 +295,11 @@ 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)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -248,11 +316,11 @@ 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)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -265,15 +333,19 @@ 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)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -286,15 +358,19 @@ 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)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
@@ -311,153 +387,69 @@ function deleteUser (id) {
})
}
function updatePassword (password) {
// SHARE
export function getShare (url) {
url = removePrefix(url)
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.open('GET', `${store.state.baseURL}/api/share${url}`, true)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
if (request.status === 200) {
resolve(JSON.parse(request.responseText))
} else {
reject(request.status)
}
}
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) {
export function deleteShare (hash) {
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.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
if (request.status === 200) {
resolve()
} else {
reject(request.status)
}
}
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) {
export function share (url, expires = '', unit = 'hours') {
url = removePrefix(url)
url = `${store.state.baseURL}/api/share${url}`
if (expires !== '') {
url += `?expires=${expires}&unit=${unit}`
}
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.open('POST', url, true)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
if (request.status === 200) {
resolve(JSON.parse(request.responseText))
} else {
reject(request.responseStatus)
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify(data))
request.send()
})
}
export default {
delete: rm,
fetch,
checksum,
move,
put,
copy,
post,
command,
search,
download,
getUser,
newUser,
updateUser,
getUsers,
updatePassword,
updateCSS,
getCommands,
updateCommands,
removePrefix,
getPlugins,
updatePlugins,
deleteUser
}

View File

@@ -16,23 +16,23 @@ function loggedIn () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/auth/renew`, true)
request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`)
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`)
request.onload = () => {
if (request.status === 200) {
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()
})
}
function login (user, password) {
let data = {username: user, password: password}
function login (user, password, captcha) {
let data = {username: user, password: password, recaptcha: captcha}
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/auth/get`, true)
@@ -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

@@ -16,7 +16,7 @@ function loading (button) {
}, 100)
}
function done (button, success = true) {
function done (button) {
let el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
@@ -33,7 +33,34 @@ function done (button, success = true) {
}, 100)
}
function success (button) {
let el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
console.log('Error getting button ' + button)
return
}
el.style.opacity = 0
setTimeout(() => {
el.classList.remove('spin')
el.innerHTML = 'done'
el.style.opacity = 1
setTimeout(() => {
el.style.opacity = 0
setTimeout(() => {
el.innerHTML = el.dataset.icon
el.style.opacity = 1
}, 100)
}, 500)
}, 100)
}
export default {
loading,
done
done,
success
}

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,10 +30,10 @@
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 api from '@/utils/api'
import Preview from '@/components/files/Preview'
import Listing from '@/components/files/Listing'
import Editor from '@/components/files/Editor'
import * as api from '@/utils/api'
import { mapGetters, mapState, mapMutations } from 'vuex'
export default {
@@ -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

@@ -0,0 +1,43 @@
<template>
<div>
<div id="progress">
<div v-bind:style="{ width: $store.state.progress + '%' }"></div>
</div>
<site-header></site-header>
<sidebar></sidebar>
<main>
<router-view @css="$emit('update:css')"></router-view>
</main>
<prompts></prompts>
</div>
</template>
<script>
import Search from '@/components/Search'
import Sidebar from '@/components/Sidebar'
import Prompts from '@/components/prompts/Prompts'
import SiteHeader from '@/components/Header'
export default {
name: 'layout',
components: {
Search,
Sidebar,
SiteHeader,
Prompts
},
watch: {
'$route': function () {
this.$store.commit('resetSelected')
this.$store.commit('multiple', false)
if (this.$store.state.show !== 'success') this.$store.commit('closeHovers')
}
},
mounted () {
this.$emit('update:css')
},
beforeDestroy () {
this.$emit('clean:css')
}
}
</script>

View File

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

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>File Manager</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
<meta name="theme-color" content="#2979ff">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
<style>
* {
box-sizing: border-box
}
body {
font-family: Arial, sans-serif;
color: #6f6f6f;
background: #f8f8f8;
}
body > div {
text-align: center;
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
display: block;
border-radius: 0.2em;
padding: 2em 3em;
}
body > a * {
margin: 0;
}
</style>
</head>
<body>
<div><h1>404 Not Found</h1></div>
</body>
</html>

View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>{{ .File.Name }}</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
<meta name="theme-color" content="#2979ff">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
<style>
* {
box-sizing: border-box
}
body {
font-family: Arial, sans-serif;
color: #6f6f6f;
background: #f8f8f8;
}
a {
text-decoration: none;
color: inherit;
}
body > a {
text-align: center;
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
display: block;
border-radius: 0.2em;
width: 90%;
max-width: 25em;
}
body > a > div:first-child {
width: 100%;
padding: 1em;
cursor: pointer;
background: #ffffff;
color: rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
body > a > div:last-child {
padding: 2em 3em;
}
body > a * {
margin: 0;
}
body > a h1 {
margin-top: .2em;
}
</style>
</head>
<body>
<a href="?dl=1">
<div>Download {{ if .File.IsDir }}Folder{{ else }}File{{ end }}</div>
<div>
{{ if .File.IsDir -}}
<svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
{{ else -}}
<svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
{{ end -}}
<h1>{{ .File.Name }}</h1>
</div>
</a>
</body>
</html>

View File

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

26
bolt/config.go Normal file
View File

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

66
bolt/share.go Normal file
View File

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

90
bolt/users.go Normal file
View File

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

View File

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

View File

@@ -1,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,160 +1,15 @@
package hugo
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/plugins"
"github.com/hacdias/fileutils"
"github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error {
configs, err := parse(c)
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next}
})
return nil
}
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
var (
configs []*filemanager.FileManager
)
for c.Next() {
// hugo [directory] [admin] {
// database path
// }
directory := "."
admin := "/admin"
database := ""
// 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: 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.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",
@@ -166,3 +21,32 @@ type plugin struct {
Next httpserver.Handler
Configs []*filemanager.FileManager
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs {
// Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue
}
h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil
}
return f.Next.ServeHTTP(w, r)
}
// setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "hugo")
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next}
})
return nil
}

52
caddy/jekyll/jekyll.go Normal file
View File

@@ -0,0 +1,52 @@
package jekyll
import (
"net/http"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("jekyll", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
type plugin struct {
Next httpserver.Handler
Configs []*filemanager.FileManager
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs {
// Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue
}
h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil
}
return f.Next.ServeHTTP(w, r)
}
// setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "jekyll")
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next}
})
return nil
}

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

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

View File

@@ -10,28 +10,40 @@ import (
"path/filepath"
"strings"
"github.com/asdine/storm"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/hacdias/filemanager/plugins"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt"
h "github.com/hacdias/filemanager/http"
"github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
)
var (
addr string
config string
database string
scope string
commands string
logfile string
plugin string
port int
allowCommands bool
allowEdit bool
allowNew bool
addr string
config string
database string
scope string
commands string
logfile string
staticg string
locale string
baseurl string
prefixurl string
viewMode string
recaptchakey string
recaptchasecret string
port int
noAuth bool
allowCommands bool
allowEdit bool
allowNew bool
allowPublish bool
showVer bool
)
func init() {
@@ -41,11 +53,20 @@ func init() {
flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file")
flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file")
flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users")
flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL")
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL")
flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users")
flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key")
flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret")
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users")
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users")
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users")
flag.StringVar(&plugin, "plugin", "", "Plugin you want to enable")
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
flag.StringVar(&locale, "locale", "en", "Default locale for new users")
flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
}
func setupViper() {
@@ -58,7 +79,15 @@ func setupViper() {
viper.SetDefault("AllowCommmands", true)
viper.SetDefault("AllowEdit", true)
viper.SetDefault("AllowNew", true)
viper.SetDefault("Plugin", "")
viper.SetDefault("AllowPublish", true)
viper.SetDefault("StaticGen", "")
viper.SetDefault("Locale", "en")
viper.SetDefault("NoAuth", false)
viper.SetDefault("BaseURL", "")
viper.SetDefault("PrefixURL", "")
viper.SetDefault("ViewMode", "mosaic")
viper.SetDefault("ReCaptchaKey", "")
viper.SetDefault("ReCaptchaSecret", "")
viper.BindPFlag("Port", flag.Lookup("port"))
viper.BindPFlag("Address", flag.Lookup("address"))
@@ -69,16 +98,33 @@ func setupViper() {
viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
viper.BindPFlag("Plugin", flag.Lookup("plugin"))
viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish"))
viper.BindPFlag("Locale", flag.Lookup("locale"))
viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key"))
viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret"))
viper.SetConfigName("filemanager")
viper.AddConfigPath(".")
}
func printVersion() {
fmt.Println("filemanager version", filemanager.Version)
os.Exit(0)
}
func main() {
setupViper()
flag.Parse()
if showVer {
printVersion()
}
// Add a configuration file if set.
if config != "" {
ext := filepath.Ext(config)
@@ -118,40 +164,6 @@ func main() {
})
}
// Create a File Manager instance.
fm, err := filemanager.New(viper.GetString("Database"), filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
CSS: "",
FileSystem: fileutils.Dir(viper.GetString("Scope")),
})
if err != nil {
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)
@@ -163,7 +175,75 @@ func main() {
fmt.Println("Listening on", listener.Addr().String())
// Starts the server.
if err := http.Serve(listener, fm); err != nil {
if err := http.Serve(listener, handler()); err != nil {
log.Fatal(err)
}
}
func handler() http.Handler {
db, err := storm.Open(viper.GetString("Database"))
if err != nil {
log.Fatal(err)
}
fm := &filemanager.FileManager{
NoAuth: viper.GetBool("NoAuth"),
BaseURL: viper.GetString("BaseURL"),
PrefixURL: viper.GetString("PrefixURL"),
ReCaptchaKey: viper.GetString("ReCaptchaKey"),
ReCaptchaSecret: viper.GetString("ReCaptchaSecret"),
DefaultUser: &filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
Scope: viper.GetString("Scope"),
FileSystem: fileutils.Dir(viper.GetString("Scope")),
ViewMode: viper.GetString("ViewMode"),
},
Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope)
},
}
err = fm.Setup()
if err != nil {
log.Fatal(err)
}
switch viper.GetString("StaticGen") {
case "hugo":
hugo := &staticgen.Hugo{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{},
CleanPublic: true,
}
if err = fm.Attach(hugo); err != nil {
log.Fatal(err)
}
case "jekyll":
jekyll := &staticgen.Jekyll{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"},
CleanPublic: true,
}
if err = fm.Attach(jekyll); err != nil {
log.Fatal(err)
}
}
return h.Handler(fm)
}

73
doc.go Normal file
View File

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

74
file.go
View File

@@ -7,7 +7,6 @@ import (
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"errors"
"hash"
"io"
"io/ioutil"
@@ -20,16 +19,12 @@ import (
"strings"
"time"
"github.com/spf13/hugo/parser"
"github.com/gohugoio/hugo/parser"
)
var (
errInvalidOption = errors.New("Invalid option")
)
// file contains the information about a particular file or directory.
type file struct {
// Indicates the Kind of view on the front-end (listing, editor or preview).
// File contains the information about a particular file or directory.
type File struct {
// Indicates the Kind of view on the front-end (Listing, editor or preview).
Kind string `json:"kind"`
// The name of the file.
Name string `json:"name"`
@@ -54,37 +49,35 @@ type file struct {
// Stores the content of a text file.
Content string `json:"content,omitempty"`
*listing `json:",omitempty"`
*Listing `json:",omitempty"`
Metadata string `json:"metadata,omitempty"`
Language string `json:"language,omitempty"`
}
// A listing is the context used to fill out a template.
type listing struct {
// A Listing is the context used to fill out a template.
type Listing struct {
// The items (files and folders) in the path.
Items []*file `json:"items"`
// The number of directories in the listing.
Items []*File `json:"items"`
// The number of directories in the Listing.
NumDirs int `json:"numDirs"`
// The number of files (items that aren't directories) in the listing.
// The number of files (items that aren't directories) in the Listing.
NumFiles int `json:"numFiles"`
// Which sorting order is used.
Sort string `json:"sort"`
// And which order.
Order string `json:"order"`
// Displays in mosaic or list.
Display string `json:"display"`
}
// getInfo gets the file information and, in case of error, returns the
// GetInfo gets the file information and, in case of error, returns the
// respective HTTP error code
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
func GetInfo(url *url.URL, c *FileManager, u *User) (*File, error) {
var err error
i := &file{
i := &File{
URL: "/files" + url.String(),
VirtualPath: url.Path,
Path: filepath.Join(string(u.FileSystem), url.Path),
Path: filepath.Join(u.Scope, url.Path),
}
info, err := u.FileSystem.Stat(url.Path)
@@ -106,11 +99,11 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
return i, nil
}
// getListing gets the information about a specific directory and its files.
func (i *file) getListing(c *RequestContext, r *http.Request) error {
// GetListing gets the information about a specific directory and its files.
func (i *File) GetListing(u *User, r *http.Request) error {
// Gets the directory information using the Virtual File System of
// the user configuration.
f, err := c.User.FileSystem.OpenFile(c.FI.VirtualPath, os.O_RDONLY, 0)
f, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
if err != nil {
return err
}
@@ -123,7 +116,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
}
var (
fileinfos []*file
fileinfos []*File
dirCount, fileCount int
)
@@ -134,7 +127,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
for _, f := range files {
name := f.Name()
allowed := c.User.Allowed("/" + name)
allowed := u.Allowed("/" + name)
if !allowed {
continue
@@ -150,7 +143,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
// Absolute URL
url := url.URL{Path: baseurl + name}
i := &file{
i := &File{
Name: f.Name(),
Size: f.Size(),
ModTime: f.ModTime(),
@@ -166,7 +159,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
fileinfos = append(fileinfos, i)
}
i.listing = &listing{
i.Listing = &Listing{
Items: fileinfos,
NumDirs: dirCount,
NumFiles: fileCount,
@@ -175,8 +168,8 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
return nil
}
// getEditor gets the editor based on a Info struct
func (i *file) getEditor() error {
// GetEditor gets the editor based on a Info struct
func (i *File) GetEditor() error {
i.Language = editorLanguage(i.Extension)
// If the editor will hold only content, leave now.
if editorMode(i.Language) == "content" {
@@ -205,7 +198,7 @@ func (i *file) getEditor() error {
// GetFileType obtains the mimetype and converts it to a simple
// type nomenclature.
func (i *file) GetFileType(checkContent bool) error {
func (i *File) GetFileType(checkContent bool) error {
var content []byte
var err error
@@ -283,7 +276,8 @@ End:
return nil
}
func (i file) Checksum(kind string) (string, error) {
// Checksum retrieves the checksum of a file.
func (i File) Checksum(algo string) (string, error) {
file, err := os.Open(i.Path)
if err != nil {
return "", err
@@ -293,7 +287,7 @@ func (i file) Checksum(kind string) (string, error) {
var h hash.Hash
switch kind {
switch algo {
case "md5":
h = md5.New()
case "sha1":
@@ -303,7 +297,7 @@ func (i file) Checksum(kind string) (string, error) {
case "sha512":
h = sha512.New()
default:
return "", errInvalidOption
return "", ErrInvalidOption
}
_, err = io.Copy(h, file)
@@ -315,12 +309,12 @@ func (i file) Checksum(kind string) (string, error) {
}
// CanBeEdited checks if the extension of a file is supported by the editor
func (i file) CanBeEdited() bool {
func (i File) CanBeEdited() bool {
return i.Type == "text"
}
// ApplySort applies the sort order using .Order and .Sort
func (l listing) ApplySort() {
func (l Listing) ApplySort() {
// Check '.Order' to know how to sort
if l.Order == "desc" {
switch l.Sort {
@@ -349,10 +343,10 @@ func (l listing) ApplySort() {
}
}
// Implement sorting for listing
type byName listing
type bySize listing
type byModified listing
// Implement sorting for Listing
type byName Listing
type bySize Listing
type byModified Listing
// By Name
func (l byName) Len() int {

View File

@@ -1,60 +1,9 @@
// Package filemanager provides a web interface to access your files
// wherever you are. To use this package as a middleware for your app,
// you'll need to create a filemanager instance:
//
// m, err := filemanager.New(database, user)
//
// Where 'user' contains the default options for new users. You can just
// use 'filemanager.DefaultUser' or create yourself a default user:
//
// m, err := filemanager.New(database, filemanager.User{
// Admin: false,
// AllowCommands: false,
// AllowEdit: true,
// AllowNew: true,
// Commands: []string{
// "git",
// },
// Rules: []*filemanager.Rule{},
// CSS: "",
// FileSystem: webdav.Dir("/path/to/files"),
// })
//
// The credentials for the first user are always 'admin' for both the user and
// the password, and they can be changed later through the settings. The first
// user is always an Admin and has all of the permissions set to 'true'.
//
// Then, you should set the Prefix URL and the Base URL, using the following
// functions:
//
// m.SetBaseURL("/")
// m.SetPrefixURL("/")
//
// The Prefix URL is a part of the path that is already stripped from the
// r.URL.Path variable before the request arrives to File Manager's handler.
// This is a function that will rarely be used. You can see one example on Caddy
// filemanager plugin.
//
// The Base URL is the URL path where you want File Manager to be available in. If
// you want to be available at the root path, you should call:
//
// m.SetBaseURL("/")
//
// But if you want to access it at '/admin', you would call:
//
// m.SetBaseURL("/admin")
//
// Now, that you already have a File Manager instance created, you just need to
// add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
// We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
//
// One simple implementation for this, at port 80, in the root of the domain, would be:
//
// http.ListenAndServe(":80", m)
package filemanager
import (
"crypto/rand"
"errors"
"fmt"
"log"
"net/http"
"os"
@@ -62,32 +11,46 @@ import (
"reflect"
"regexp"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
rice "github.com/GeertJohan/go.rice"
"github.com/asdine/storm"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/robfig/cron"
)
// Version is the current File Manager version.
const Version = "1.3.5"
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")
plugins = map[string]Plugin{}
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyRequest = errors.New("request body is empty")
ErrEmptyPassword = errors.New("password is empty")
ErrEmptyUsername = errors.New("username is empty")
ErrEmptyScope = errors.New("scope is empty")
ErrWrongDataType = errors.New("wrong data type")
ErrInvalidUpdateField = errors.New("invalid field to update")
ErrInvalidOption = errors.New("Invalid option")
)
// FileManager is a file manager instance. It should be creating using the
// 'New' function and not directly.
type FileManager struct {
// The BoltDB database for this instance.
db *storm.DB
// Cron job to manage schedulings.
Cron *cron.Cron
// The key used to sign the JWT tokens.
key []byte
Key []byte
// The static assets.
assets *rice.Box
Assets *rice.Box
// The Store is used to manage users, shareable links and
// other stuff that is saved on the database.
Store *Store
// PrefixURL is a part of the URL that is already trimmed from the request URL before it
// arrives to our handlers. It may be useful when using File Manager as a middleware
@@ -99,194 +62,134 @@ 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
// ReCaptcha Site key and secret.
ReCaptchaKey string
ReCaptchaSecret string
// StaticGen is the static websit generator handler.
StaticGen StaticGen
// The Default User needed to build the New User page.
DefaultUser *User
// Users is a map with the different configurations for each user.
Users map[string]*User
// A map of events to a slice of commands.
Commands map[string][]string
// The options of the plugins that have been plugged into this instance.
Plugins map[string]interface{}
// Global stylesheet.
CSS string
// NewFS should build a new file system for a given path.
NewFS FSBuilder
}
var commandEvents = []string{
"before_save",
"after_save",
"before_publish",
"after_publish",
"before_copy",
"after_copy",
"before_rename",
"after_rename",
"before_upload",
"after_upload",
"before_delete",
"after_delete",
}
// Command is a command function.
type Command func(r *http.Request, m *FileManager, u *User) error
// User contains the configuration for each user.
type User struct {
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// FSBuilder is the File System Builder.
type FSBuilder func(scope string) FileSystem
// Username is the user username used to login.
Username string `json:"username" storm:"index,unique"`
// The hashed password. This never reaches the front-end because it's temporarily
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// FileSystem is the virtual file system the user has access.
FileSystem fileutils.Dir `json:"filesystem"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
Permissions map[string]bool `json:"permissions"` // Permissions added by plugins
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
}
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
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.
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.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
Permissions: map[string]bool{},
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
FileSystem: fileutils.Dir("."),
}
// New creates a new File Manager instance. If 'database' file already
// exists, it will load the users from there. Otherwise, a new user
// will be created using the 'base' variable. The 'base' User should
// not have the Password field hashed.
func New(database string, base User) (*FileManager, error) {
// Setup loads the configuration from the database and configures
// the Assets and the Cron job. It must always be run after
// creating a File Manager object.
func (m *FileManager) Setup() error {
// Creates a new File Manager instance with the Users
// map and Assets box.
m := &FileManager{
Users: map[string]*User{},
Plugins: map[string]interface{}{},
assets: rice.MustFindBox("./assets/dist"),
}
// Tries to open a database on the location provided. This
// function will automatically create a new one if it doesn't
// exist.
db, err := storm.Open(database)
if err != nil {
return nil, err
}
m.Assets = rice.MustFindBox("./assets/dist")
m.Cron = cron.New()
// Tries to get the encryption key from the database.
// If it doesn't exist, create a new one of 256 bits.
err = db.Get("config", "key", &m.key)
if err != nil && err == storm.ErrNotFound {
err := m.Store.Config.Get("key", &m.Key)
if err != nil && err == ErrNotExist {
var bytes []byte
bytes, err = generateRandomBytes(64)
bytes, err = GenerateRandomBytes(64)
if err != nil {
return nil, err
return err
}
m.key = bytes
err = db.Set("config", "key", m.key)
m.Key = bytes
err = m.Store.Config.Save("key", m.Key)
}
if err != nil {
return nil, err
return err
}
// Get the global CSS.
err = m.Store.Config.Get("css", &m.CSS)
if err != nil && err == ErrNotExist {
err = m.Store.Config.Save("css", "")
}
if err != nil {
return err
}
// Tries to get the event commands from the database.
// If they don't exist, initialize them.
err = db.Get("config", "commands", &m.Commands)
if err != nil && err == storm.ErrNotFound {
m.Commands = map[string][]string{
"before_save": {},
"after_save": {},
err = m.Store.Config.Get("commands", &m.Commands)
if err == nil {
// Add hypothetically new command handlers.
for _, command := range commandEvents {
if _, ok := m.Commands[command]; ok {
continue
}
m.Commands[command] = []string{}
}
err = db.Set("config", "commands", m.Commands)
}
if err != nil && err == ErrNotExist {
m.Commands = map[string][]string{}
// Initialize the command handlers.
for _, command := range commandEvents {
m.Commands[command] = []string{}
}
err = m.Store.Config.Save("commands", m.Commands)
}
if err != nil {
return nil, err
return err
}
// Tries to fetch the users from the database and if there are
// any, add them to the current File Manager instance.
var users []User
err = db.All(&users)
if err != nil {
return nil, err
}
for i := range users {
m.Users[users[i].Username] = &users[i]
// Tries to fetch the users from the database.
users, err := m.Store.Users.Gets(m.NewFS)
if err != nil && err != ErrNotExist {
return err
}
// If there are no users in the database, it creates a new one
// based on 'base' User that must be provided by the function caller.
if len(users) == 0 {
u := base
u := *m.DefaultUser
u.Username = "admin"
// Hashes the password.
u.Password, err = hashPassword("admin")
u.Password, err = HashPassword("admin")
if err != nil {
return nil, err
return err
}
// The first user must be an administrator.
@@ -294,23 +197,29 @@ func New(database string, base User) (*FileManager, error) {
u.AllowCommands = true
u.AllowNew = true
u.AllowEdit = true
u.AllowPublish = true
// Saves the user to the database.
if err := db.Save(&u); err != nil {
return nil, err
if err := m.Store.Users.Save(&u); err != nil {
return err
}
m.Users[u.Username] = &u
}
// Attaches db to this File Manager instance.
m.db = db
// TODO: remove this after 1.5
for _, user := range users {
if user.ViewMode != "list" && user.ViewMode != "mosaic" {
user.ViewMode = "list"
m.Store.Users.Update(user, "ViewMode")
}
}
// Create the default user, making a copy of the base.
base.Username = ""
base.Password = ""
m.DefaultUser = &base
return m, nil
m.DefaultUser.Username = ""
m.DefaultUser.Password = ""
m.Cron.AddFunc("@hourly", m.ShareCleaner)
m.Cron.Start()
return nil
}
// RootURL returns the actual URL where
@@ -337,148 +246,50 @@ func (m *FileManager) SetBaseURL(url string) {
m.BaseURL = strings.TrimSuffix(url, "/")
}
// ActivatePlugin activates a plugin to a File Manager instance and
// loads its options from the database.
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(name + " plugin is already activated")
}
err := m.db.Get("plugins", name, &plugin)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("plugin", name, plugin)
// Attach attaches a static generator to the current File Manager.
func (m *FileManager) Attach(s StaticGen) error {
if reflect.TypeOf(s).Kind() != reflect.Ptr {
return errors.New("data should be a pointer to interface, not interface")
}
err := s.Setup()
if err != nil {
return err
}
// Register the command event hooks.
for _, evt := range plugin.CommandEvents {
if _, ok := m.Commands[evt]; ok {
continue
}
m.StaticGen = s
m.Commands[evt] = []string{}
err = m.Store.Config.Get("staticgen_"+s.Name(), s)
if err == ErrNotExist {
return m.Store.Config.Save("staticgen_"+s.Name(), s)
}
err = m.db.Set("config", "commands", m.Commands)
return err
}
// ShareCleaner removes sharing links that are no longer active.
// This function is set to run periodically.
func (m FileManager) ShareCleaner() {
// Get all links.
links, err := m.Store.Share.Gets()
if err != nil {
return err
log.Print(err)
return
}
// 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
}
// 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 {
if _, ok := m.DefaultUser.Permissions[name]; ok {
return nil
}
// Add the default value for this permission on the default user.
m.DefaultUser.Permissions[name] = value
for _, u := range m.Users {
// Bypass the user if it is already defined.
if _, ok := u.Permissions[name]; ok {
continue
}
if u.Permissions == nil {
u.Permissions = m.DefaultUser.Permissions
}
if u.Admin {
u.Permissions[name] = true
}
err := m.db.Save(u)
if err != nil {
return err
}
}
return nil
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
// Compatible with http.Handler.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := serveHTTP(&RequestContext{
FM: m,
User: nil,
FI: nil,
}, w, r)
if code != 0 {
w.WriteHeader(code)
if err != nil {
log.Print(err)
w.Write([]byte(err.Error()))
} else {
log.Print(code)
w.Write([]byte(http.StatusText(code)))
}
}
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule
i := len(u.Rules) - 1
for i >= 0 {
rule = u.Rules[i]
if rule.Regex {
if rule.Regexp.MatchString(url) {
return rule.Allow
// Find the expired ones.
for i := range links {
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
err = m.Store.Share.Delete(links[i].Hash)
if err != nil {
log.Print(err)
}
} else if strings.HasPrefix(url, rule.Path) {
return rule.Allow
}
i--
}
return true
}
// MatchString checks if this string matches the regular expression.
func (r *Regexp) MatchString(s string) bool {
if r.regexp == nil {
r.regexp = regexp.MustCompile(r.Raw)
}
return r.regexp.MatchString(s)
}
// Runner runs the commands for a certain event type.
func (m FileManager) Runner(event string, path string) error {
func (m FileManager) Runner(event string, path string, destination string, user *User) error {
commands := []string{}
// Get the commands from the File Manager instance itself.
@@ -503,7 +314,15 @@ func (m FileManager) Runner(event string, path string) error {
}
cmd := exec.Command(command, args...)
cmd.Env = append(os.Environ(), "file="+path)
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
cmd.Env = append(cmd.Env, fmt.Sprintf("ROOT=%s", string(user.Scope)))
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", event))
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
if destination != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", destination))
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -525,3 +344,217 @@ func (m FileManager) Runner(event string, path string) error {
return nil
}
// DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
LockPassword: false,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
Scope: ".",
FileSystem: fileutils.Dir("."),
ViewMode: "mosaic",
}
// User contains the configuration for each user.
type User struct {
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// Username is the user username used to login.
Username string `json:"username" storm:"index,unique"`
// The hashed password. This never reaches the front-end because it's temporarily
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// Scope is the path the user has access to.
Scope string `json:"filesystem"`
// FileSystem is the virtual file system the user has access.
FileSystem FileSystem `json:"-"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// Prevents the user to change its password.
LockPassword bool `json:"lockPassword"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
// User view mode for files and folders.
ViewMode string `json:"viewMode"`
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule
i := len(u.Rules) - 1
for i >= 0 {
rule = u.Rules[i]
if rule.Regex {
if rule.Regexp.MatchString(url) {
return rule.Allow
}
} else if strings.HasPrefix(url, rule.Path) {
return rule.Allow
}
i--
}
return true
}
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
// MatchString checks if this string matches the regular expression.
func (r *Regexp) MatchString(s string) bool {
if r.regexp == nil {
r.regexp = regexp.MustCompile(r.Raw)
}
return r.regexp.MatchString(s)
}
// ShareLink is the information needed to build a shareable link.
type ShareLink struct {
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
Expires bool `json:"expires"`
ExpireDate time.Time `json:"expireDate"`
}
// Store is a collection of the stores needed to get
// and save information.
type Store struct {
Users UsersStore
Config ConfigStore
Share ShareStore
}
// UsersStore is the interface to manage users.
type UsersStore interface {
Get(id int, builder FSBuilder) (*User, error)
GetByUsername(username string, builder FSBuilder) (*User, error)
Gets(builder FSBuilder) ([]*User, error)
Save(u *User) error
Update(u *User, fields ...string) error
Delete(id int) error
}
// ConfigStore is the interface to manage configuration.
type ConfigStore interface {
Get(name string, to interface{}) error
Save(name string, from interface{}) error
}
// ShareStore is the interface to manage share links.
type ShareStore interface {
Get(hash string) (*ShareLink, error)
GetPermanent(path string) (*ShareLink, error)
GetByPath(path string) ([]*ShareLink, error)
Gets() ([]*ShareLink, error)
Save(s *ShareLink) error
Delete(hash string) error
}
// StaticGen is a static website generator.
type StaticGen interface {
SettingsPath() string
Name() string
Setup() error
Hook(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
}
// FileSystem is the interface to work with the file system.
type FileSystem interface {
Mkdir(name string, perm os.FileMode) error
OpenFile(name string, flag int, perm os.FileMode) (*os.File, error)
RemoveAll(name string) error
Rename(oldName, newName string) error
Stat(name string) (os.FileInfo, error)
Copy(src, dst string) error
}
// Context contains the needed information to make handlers work.
type Context struct {
*FileManager
User *User
File *File
// On API handlers, Router is the APi handler we want.
Router string
}
// HashPassword generates an hash from a password using bcrypt.
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPasswordHash compares a password with an hash to check if they match.
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// GenerateRandomBytes returns securely generated random bytes.
// It will return an fm.Error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}

View File

@@ -1,49 +0,0 @@
package filemanager
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hacdias/fileutils"
)
type test struct {
*FileManager
Temp string
}
func (t test) Clean() {
t.db.Close()
os.RemoveAll(t.Temp)
}
func newTest(t *testing.T) *test {
temp, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatalf("Error creating temporary directory: %v", err)
}
scope := filepath.Join(temp, "scope")
database := filepath.Join(temp, "database.db")
err = fileutils.CopyDir("./testdata", scope)
if err != nil {
t.Fatalf("Error copying the test data: %v", err)
}
user := DefaultUser
user.FileSystem = fileutils.Dir(scope)
fm, err := New(database, user)
if err != nil {
t.Fatalf("Error creating a file manager instance: %v", err)
}
return &test{
FileManager: fm,
Temp: temp,
}
}

257
http.go
View File

@@ -1,257 +0,0 @@
package filemanager
import (
"encoding/json"
"html/template"
"net/http"
"os"
"strings"
)
// RequestContext contains the needed information to make handlers work.
type RequestContext struct {
User *User
FM *FileManager
FI *file
// On API handlers, Router is the APi handler we want.
Router string
}
// serveHTTP is the main entry point of this HTML application.
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)
if len(p) >= len(r.URL.Path) && c.FM.BaseURL != "" {
return http.StatusNotFound, nil
}
r.URL.Path = p
// Check if this request is made to the service worker. If so,
// pass it through a template to add the needed variables.
if r.URL.Path == "/sw.js" {
return renderFile(
w,
c.FM.assets.MustString("sw.js"),
"application/javascript",
c,
)
}
// Checks if this request is made to the static assets folder. If so, and
// if it is a GET request, returns with the asset. Otherwise, returns
// a status not implemented.
if matchURL(r.URL.Path, "/static") {
if r.Method != http.MethodGet {
return http.StatusNotImplemented, nil
}
return staticHandler(c, w, r)
}
// Checks if this request is made to the API and directs to the
// API handler if so.
if matchURL(r.URL.Path, "/api") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
return apiHandler(c, w, r)
}
// Any other request should show the index.html file.
w.Header().Set("x-frame-options", "SAMEORIGIN")
w.Header().Set("x-content-type", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block")
return renderFile(
w,
c.FM.assets.MustString("index.html"),
"text/html",
c,
)
}
// 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)
return 0, nil
}
return renderFile(
w,
c.FM.assets.MustString("static/manifest.json"),
"application/json",
c,
)
}
// apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth/get" {
return authHandler(c, w, r)
}
if r.URL.Path == "/auth/renew" {
return renewAuthHandler(c, w, r)
}
valid, _ := validateAuth(c, r)
if !valid {
return http.StatusForbidden, nil
}
c.Router, r.URL.Path = splitURL(r.URL.Path)
if !c.User.Allowed(r.URL.Path) {
return http.StatusForbidden, nil
}
for p := range c.FM.Plugins {
code, err := plugins[p].Handler.Before(c, w, r)
if code != 0 || err != nil {
return code, err
}
}
if c.Router == "checksum" || c.Router == "download" {
var err error
c.FI, err = getInfo(r.URL, c.FM, c.User)
if err != nil {
return errorToHTTP(err, false), err
}
}
var code int
var err error
switch c.Router {
case "download":
code, err = downloadHandler(c, w, r)
case "checksum":
code, err = checksumHandler(c, w, r)
case "command":
code, err = command(c, w, r)
case "search":
code, err = search(c, w, r)
case "resource":
code, err = resourceHandler(c, w, r)
case "users":
code, err = usersHandler(c, w, r)
case "commands":
code, err = commandsHandler(c, w, r)
case "plugins":
code, err = pluginsHandler(c, w, r)
default:
code = http.StatusNotFound
}
if code >= 300 || err != nil {
return code, err
}
for p := range c.FM.Plugins {
code, err := plugins[p].Handler.After(c, w, r)
if code != 0 || err != nil {
return code, err
}
}
return code, err
}
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo")
val, err := c.FI.Checksum(query)
if err == errInvalidOption {
return http.StatusBadRequest, err
} else if err != nil {
return http.StatusInternalServerError, err
}
w.Write([]byte(val))
return 0, nil
}
// splitURL splits the path and returns everything that stands
// before the first slash and everything that goes after.
func splitURL(path string) (string, string) {
if path == "" {
return "", ""
}
path = strings.TrimPrefix(path, "/")
i := strings.Index(path, "/")
if i == -1 {
return "", path
}
return path[0:i], path[i:]
}
// renderFile renders a file using a template with some needed variables.
func renderFile(w http.ResponseWriter, file string, contentType string, c *RequestContext) (int, error) {
tpl := template.Must(template.New("file").Parse(file))
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
var javascript = ""
for name := range c.FM.Plugins {
javascript += plugins[name].JavaScript + "\n"
}
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.FM.RootURL(),
"JavaScript": template.JS(javascript),
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// renderJSON prints the JSON version of data to the browser.
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
marsh, err := json.Marshal(data)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write(marsh); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
// matchURL checks if the first URL matches the second.
func matchURL(first, second string) bool {
first = strings.ToLower(first)
second = strings.ToLower(second)
return strings.HasPrefix(first, second)
}
// errorToHTTP converts errors to HTTP Status Code.
func errorToHTTP(err error, gone bool) int {
switch {
case err == nil:
return http.StatusOK
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err):
if !gone {
return http.StatusNotFound
}
return http.StatusGone
case os.IsExist(err):
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}

View File

@@ -1,22 +1,67 @@
package filemanager
package http
import (
"crypto/rand"
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
fm "github.com/hacdias/filemanager"
)
type cred struct {
Password string `json:"password"`
Username string `json:"username"`
Recaptcha string `json:"recaptcha"`
}
// recaptcha checks the recaptcha code.
func recaptcha(secret string, response string) (bool, error) {
api := "https://www.google.com/recaptcha/api/siteverify"
body := url.Values{}
body.Set("secret", secret)
body.Add("response", response)
client := &http.Client{}
resp, err := client.Post(api, "application/x-www-form-urlencoded", bytes.NewBufferString(body.Encode()))
if err != nil {
return false, err
}
if resp.StatusCode != http.StatusOK {
return false, nil
}
var data struct {
Success bool `json:"success"`
ChallengeTS time.Time `json:"challenge_ts"`
Hostname string `json:"hostname"`
ErrorCodes interface{} `json:"error-codes"`
}
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return false, err
}
return data.Success, nil
}
// authHandler proccesses the authentication for the user.
func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// NoAuth instances shouldn't call this method.
if c.NoAuth {
return 0, nil
}
// Receive the credentials from the request and unmarshal them.
var cred User
var cred cred
if r.Body == nil {
return http.StatusForbidden, nil
}
@@ -26,14 +71,27 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
return http.StatusForbidden, nil
}
// If ReCaptcha is enabled, check the code.
if len(c.ReCaptchaSecret) > 0 {
ok, err := recaptcha(c.ReCaptchaSecret, cred.Recaptcha)
if err != nil {
fmt.Println(err)
return http.StatusForbidden, err
}
if !ok {
return http.StatusForbidden, nil
}
}
// Checks if the user exists.
u, ok := c.FM.Users[cred.Username]
if !ok {
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
if err != nil {
return http.StatusForbidden, nil
}
// Checks if the password is correct.
if !checkPasswordHash(cred.Password, u.Password) {
if !fm.CheckPasswordHash(cred.Password, u.Password) {
return http.StatusForbidden, nil
}
@@ -43,7 +101,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
// renewAuthHandler is used when the front-end already has a JWT token
// and is checking if it is up to date. If so, updates its info.
func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func renewAuthHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
ok, u := validateAuth(c, r)
if !ok {
return http.StatusForbidden, nil
@@ -55,15 +113,15 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// claims is the JWT claims.
type claims struct {
User
fm.User
jwt.StandardClaims
}
// printToken prints the final JWT token to the user.
func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
func printToken(c *fm.Context, w http.ResponseWriter) (int, error) {
// Creates a copy of the user and removes it password
// hash so it never arrives to the user.
u := User{}
u := fm.User{}
u = *c.User
u.Password = ""
@@ -78,7 +136,7 @@ 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
@@ -86,7 +144,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
// Writes the token.
w.Header().Set("Content-Type", "cty")
w.Write([]byte(string))
w.Write([]byte(signed))
return 0, nil
}
@@ -112,9 +170,14 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
// validateAuth is used to validate the authentication and returns the
// User if it is valid.
func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
func validateAuth(c *fm.Context, r *http.Request) (bool, *fm.User) {
if c.NoAuth {
c.User = c.DefaultUser
return true, c.User
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
return c.FM.key, nil
return c.Key, nil
}
var claims claims
token, err := request.ParseFromRequestWithClaims(r,
@@ -127,38 +190,11 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
return false, nil
}
u, ok := c.FM.Users[claims.User.Username]
if !ok {
u, err := c.Store.Users.Get(claims.User.ID, c.NewFS)
if err != nil {
return false, nil
}
c.User = u
return true, u
}
// hashPassword generates an hash from a password using bcrypt.
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// checkPasswordHash compares a password with an hash to check if they match.
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// generateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}

View File

@@ -1,4 +1,4 @@
package filemanager
package http
import (
"io"
@@ -9,25 +9,26 @@ import (
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
)
// downloadHandler creates an archive in one of the supported formats (zip, tar,
// tar.gz or tar.bz2) and sends it to be downloaded.
func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func downloadHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("format")
// 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
}
@@ -46,14 +47,14 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Clean the slashes.
name = fileutils.SlashClean(name)
files = append(files, filepath.Join(c.FI.Path, 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".
if query == "true" {
if query == "true" || query == "" {
query = "zip"
}
@@ -93,7 +94,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"
}
@@ -106,7 +107,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
defer file.Close()
w.Header().Set("Content-Disposition", "attachment; filename="+name)
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
_, err = io.Copy(w, file)
return 0, err
}

346
http/http.go Normal file
View File

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

View File

@@ -1,15 +1,19 @@
package filemanager
package http
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
)
@@ -23,7 +27,7 @@ func sanitizeURL(url string) string {
return path
}
func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
@@ -33,8 +37,8 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return resourceDeleteHandler(c, w, r)
case http.MethodPut:
// Before save command handler.
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
if err := c.FM.Runner("before_save", path); err != nil {
path := filepath.Join(c.User.Scope, r.URL.Path)
if err := c.Runner("before_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
@@ -44,7 +48,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, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
@@ -58,11 +62,11 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusNotImplemented, nil
}
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file.
f, err := getInfo(r.URL, c.FM, c.User)
f, err := fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
// If it's a dir and the path doesn't end with a trailing slash,
@@ -73,13 +77,13 @@ 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)
}
// Tries to get the file type.
if err = f.GetFileType(true); err != nil {
return errorToHTTP(err, true), err
return ErrorToHTTP(err, true), err
}
// Serve a preview if the file can't be edited or the
@@ -93,26 +97,26 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
f.Kind = "editor"
// Tries to get the editor data.
if err = f.getEditor(); err != nil {
if err = f.GetEditor(); err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, f)
}
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.FI
func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.File
f.Kind = "listing"
// Tries to get the listing data.
if err := f.getListing(c, r); err != nil {
return errorToHTTP(err, true), err
if err := f.GetListing(c.User, r); err != nil {
return ErrorToHTTP(err, true), err
}
listing := f.listing
listing := f.Listing
// Defines the cookie scope.
cookieScope := c.FM.RootURL()
cookieScope := c.RootURL()
if cookieScope == "" {
cookieScope = "/"
}
@@ -126,27 +130,35 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
}
listing.ApplySort()
listing.Display = displayMode(w, r, cookieScope)
return renderJSON(w, f)
}
func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Prevent the removal of the root directory.
if r.URL.Path == "/" || !c.User.AllowEdit {
return http.StatusForbidden, nil
}
// Fire the before trigger.
if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
// Remove the file or folder.
err := c.User.FileSystem.RemoveAll(r.URL.Path)
if err != nil {
return errorToHTTP(err, true), err
return ErrorToHTTP(err, true), err
}
// Fire the after trigger.
if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowNew && r.Method == http.MethodPost {
return http.StatusForbidden, nil
}
@@ -155,6 +167,12 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
return http.StatusForbidden, nil
}
// Discard any invalid upload before returning to avoid connection
// reset error.
defer func() {
io.Copy(ioutil.Discard, r.Body)
}()
// Checks if the current request is for a directory and not a file.
if strings.HasSuffix(r.URL.Path, "/") {
// If the method is PUT, we return 405 Method not Allowed, because
@@ -164,46 +182,118 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
}
// Otherwise we try to create the directory.
err := c.User.FileSystem.Mkdir(r.URL.Path, 0666)
return errorToHTTP(err, false), err
err := c.User.FileSystem.Mkdir(r.URL.Path, 0776)
return ErrorToHTTP(err, false), err
}
// If using POST method, we are trying to create a new file so it is not
// desirable to override an already existent file. Thus, we check
// if the file already exists. If so, we just return a 409 Conflict.
if r.Method == http.MethodPost {
if r.Method == http.MethodPost && r.Header.Get("Action") != "override" {
if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil {
return http.StatusConflict, errors.New("There is already a file on that path")
}
}
// Fire the before trigger.
if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
// Create/Open the file.
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
defer f.Close()
// Copies the new content for the file.
_, err = io.Copy(f, r.Body)
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
// Gets the info about the file.
fi, err := f.Stat()
if err != nil {
return errorToHTTP(err, false), err
return ErrorToHTTP(err, false), err
}
// Check if this instance has a Static Generator and handles publishing
// or scheduling if it's the case.
if c.StaticGen != nil {
code, err := resourcePublishSchedule(c, w, r)
if code != 0 {
return code, err
}
}
// Writes the ETag Header.
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
w.Header().Set("ETag", etag)
// Fire the after trigger.
if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
publish := r.Header.Get("Publish")
schedule := r.Header.Get("Schedule")
if publish != "true" && schedule == "" {
return 0, nil
}
if !c.User.AllowPublish {
return http.StatusForbidden, nil
}
if publish == "true" {
return resourcePublish(c, w, r)
}
t, err := time.Parse("2006-01-02T15:04", schedule)
if err != nil {
return http.StatusInternalServerError, err
}
c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
_, err := resourcePublish(c, w, r)
if err != nil {
log.Print(err)
}
})
return http.StatusOK, nil
}
func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path)
// Before save command handler.
if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
code, err := c.StaticGen.Publish(c, w, r)
if err != nil {
return code, err
}
// Executed the before publish command.
if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err
}
return code, nil
}
// resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit {
return http.StatusForbidden, nil
}
@@ -212,7 +302,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
action := r.Header.Get("Action")
dst, err := url.QueryUnescape(dst)
if err != nil {
return errorToHTTP(err, true), err
return ErrorToHTTP(err, true), err
}
src := r.URL.Path
@@ -222,38 +312,34 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
}
if action == "copy" {
// Fire the after trigger.
if err := c.Runner("before_copy", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
// Copy the file.
err = c.User.FileSystem.Copy(src, dst)
// Fire the after trigger.
if err := c.Runner("after_copy", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
} else {
// Fire the after trigger.
if err := c.Runner("before_rename", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
// Rename the file.
err = c.User.FileSystem.Rename(src, dst)
// Fire the after trigger.
if err := c.Runner("after_rename", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err
}
}
return errorToHTTP(err, true), err
}
// displayMode obtains the display mode from the Cookie.
func displayMode(w http.ResponseWriter, r *http.Request, scope string) string {
var displayMode string
// Checks the cookie.
if displayCookie, err := r.Cookie("display"); err == nil {
displayMode = displayCookie.Value
}
// If it's invalid, set it to mosaic, which is the default.
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
displayMode = "mosaic"
}
// Set the cookie.
http.SetCookie(w, &http.Cookie{
Name: "display",
Value: displayMode,
MaxAge: 31536000,
Path: scope,
Secure: r.TLS != nil,
})
return displayMode
return ErrorToHTTP(err, true), err
}
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',

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