Compare commits

..

33 Commits

Author SHA1 Message Date
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
91 changed files with 3702 additions and 1915 deletions

View File

@@ -7,7 +7,8 @@ RUN apk add --no-cache git
RUN go get ./...
WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager
RUN go install
RUN go build -ldflags "-X main.version=$(git tag -l --points-at HEAD)"
RUN mv filemanager /go/bin/filemanager
FROM alpine:latest
COPY --from=0 /go/bin/filemanager /usr/local/bin/filemanager

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,11 @@
<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 }}">
<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">
@@ -21,14 +22,11 @@
<!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<% for (var chunk of webpack.chunks) {
<% for (var chunk of webpack.compilation.chunks) {
for (var file of chunk.files) {
if (file.match(/\.(js|css)$/)) { %>
<link rel="<%= chunk.initial?'preload':'prefetch' %>" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<!-- Plugins info -->
<script>{{ .JavaScript }}</script>
<style>
#loading {
position: fixed;

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<template>
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
<div id="input">
<button v-if="active" class="action" @click="close">
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
<i class="material-icons">arrow_back</i>
</button>
<i v-else class="material-icons">search</i>
@@ -11,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.user.noAuth">
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
<i class="material-icons">settings_applications</i>
<span>{{ $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>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</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']),
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,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,7 +1,7 @@
<template>
<button @click="change" aria-label="Switch View" title="Switch View" class="action" id="switch-view-button">
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
<i class="material-icons">{{ icon() }}</i>
<span>Switch view</span>
<span>{{ $t('buttons.switchView') }}</span>
</button>
</template>

View File

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

View File

@@ -1,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')
buttons.done(button)
this.$store.commit('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"
@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,7 +91,7 @@
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 {
@@ -215,7 +229,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,56 +238,94 @@ 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(error => { console.log(error) })
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 = []
for (let file of files) {
promises.push(api.post(this.$route.path + base + file.name, file))
promises.push(api.post(this.$route.path + base + file.name, file, overwrite))
}
Promise.all(promises)
.then(() => {
buttons.done('upload')
buttons.success('upload')
this.$store.commit('setReload', true)
})
.catch(error => {

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',

View File

@@ -1,7 +1,7 @@
<template>
<div id="previewer">
<div class="bar">
<button @click="back" class="action" aria-label="Close Preview" id="close">
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
<i class="material-icons">close</i>
</button>
@@ -11,8 +11,12 @@
<info-button></info-button>
</div>
<button class="action" @click="prev" v-show="hasPrevious"><i class="material-icons">chevron_left</i></button>
<button class="action" @click="next" v-show="hasNext"><i class="material-icons">chevron_right</i></button>
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
<i class="material-icons">chevron_left</i>
</button>
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<i class="material-icons">chevron_right</i>
</button>
<div class="preview">
<img v-if="req.type == 'image'" :src="raw()">
@@ -24,7 +28,7 @@
</video>
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object>
<a v-else-if="req.type == 'blob'" :href="download()">
<h2 class="message">Download <i class="material-icons">file_download</i></h2>
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
</a>
<pre v-else >{{ req.content }}</pre>
</div>
@@ -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',

View File

@@ -1,13 +1,19 @@
<template>
<div class="prompt">
<h3>Copy</h3>
<p>Choose the place to copy your files:</p>
<h3>{{ $t('prompts.copy') }}</h3>
<p>{{ $t('prompts.copyMessage') }}</p>
<file-list @update:selected="val => dest = val"></file-list>
<div>
<button class="ok" @click="copy">Copy</button>
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
<button class="ok"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>
@@ -15,7 +21,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,7 +51,7 @@ export default {
// Execute the promises.
api.copy(items)
.then(() => {
buttons.done('copy')
buttons.success('copy')
this.$router.push({ path: this.dest })
})
.catch(error => {

View File

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

View File

@@ -1,7 +1,8 @@
<template>
<div class="prompt" id="download">
<h3>Download files</h3>
<p>Choose the format you want to download.</p>
<h3>{{ $t('prompts.download') }}</h3>
<p>{{ $t('prompts.downloadMessage') }}</p>
<button @click="download('zip')" autofocus>zip</button>
<button @click="download('tar')" autofocus>tar</button>
<button @click="download('targz')" autofocus>tar.gz</button>
@@ -12,7 +13,7 @@
<script>
import {mapGetters, mapState} from 'vuex'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'download',

View File

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

View File

@@ -4,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',

View File

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

View File

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

View File

@@ -1,13 +1,19 @@
<template>
<div class="prompt">
<h3>Move</h3>
<p>Choose new house for your file(s)/folder(s):</p>
<h3>{{ $t('prompts.move') }}</h3>
<p>{{ $t('prompts.moveMessage') }}</p>
<file-list @update:selected="val => dest = val"></file-list>
<div>
<button class="ok" @click="move">Move</button>
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
<button class="ok"
@click="move"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>
@@ -15,7 +21,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,7 +51,7 @@ export default {
// Execute the promises.
api.move(items)
.then(() => {
buttons.done('move')
buttons.success('move')
this.$router.push({ path: this.dest })
})
.catch(error => {

View File

@@ -0,0 +1,68 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.newFile') }}</h3>
<p>{{ $t('prompts.newArchetype') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
<div>
<button class="ok"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</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(error => {
this.$store.commit('showError', error)
})
},
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)
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,24 @@
<template>
<div class="prompt">
<h3>New directory</h3>
<p>Write the name of the new directory.</p>
<h3>{{ $t('prompts.newDir') }}</h3>
<p>{{ $t('prompts.newDirMessage') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button class="ok" @click="submit">Create</button>
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
<button class="ok"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
@click="submit">{{ $t('buttons.create') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>
<script>
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'new-dir',

View File

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

View File

@@ -11,27 +11,10 @@
<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>
@@ -48,24 +31,32 @@ 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 {
@@ -90,6 +81,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,18 @@
<template>
<div class="prompt">
<h3>Rename</h3>
<p>Insert a new name for <code>{{ oldName() }}</code>:</p>
<h3>{{ $t('prompts.rename') }}</h3>
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button @click="submit" type="submit">Rename</button>
<button @click="cancel" class="cancel">Cancel</button>
<button @click="submit"
type="submit"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
</div>
</div>
</template>
@@ -13,7 +20,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',

View File

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

View File

@@ -0,0 +1,41 @@
<template>
<div class="prompt">
<h3>{{ $t('prompts.schedule') }}</h3>
<p>{{ $t('prompts.scheduleMessage') }}</p>
<input autofocus type="datetime-local" v-model="date">
<div>
<button class="ok"
@click="submit"
:aria-label="$t('buttons.schedule')"
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
<button class="cancel"
@click="close"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</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,153 @@
<template>
<div class="prompt" id="share">
<h3>{{ $t('buttons.share') }}</h3>
<p></p>
<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"
: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>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } 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')
},
methods: {
...mapMutations([ 'showError' ]),
submit: function (event) {
if (!this.time) return
share(this.url, this.time, this.unit)
.then(result => { this.links.push(result); this.sort() })
.catch(error => { this.showError(error) })
},
getPermalink (event) {
share(this.url)
.then(result => {
this.links.push(result)
this.sort()
this.hasPermanent = true
})
.catch(error => { this.showError(error) })
},
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(error => { this.showError(error) })
},
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

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

View File

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

View File

@@ -50,7 +50,7 @@ header>div div {
position: relative;
}
header > div:last-child div {
header>div:last-child div {
display: flex;
}
@@ -129,10 +129,10 @@ header .search-button {
#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 +140,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 +159,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 +172,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 +213,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;
}

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

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

View File

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

@@ -177,3 +177,32 @@
opacity: 1;
}
}
.prompt#share ul {
list-style: none;
padding: 0;
margin: 0;
}
.prompt#share ul li {
display: flex;
justify-content: space-between;
align-items: center;
}
.prompt#share ul li a {
color: #2196F3;
cursor: pointer;
margin-right: auto;
}
.prompt#share ul li .action i {
font-size: 1em;
}
.prompt#share ul li input,
.prompt#share ul li select {
padding: .2em;
margin-right: .5em;
border: 1px solid #dadada;
}

View File

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

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

@@ -0,0 +1,195 @@
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: Swicth view
toggleSidebar: Toggle sidebar
update: Update
upload: Upload
permalink: Get Permanent Link
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
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 won''t be accessible
to the user. We support regex and paths relative to the user''s scope.'
rulesHelp2: >
Each rule goes in one different line and must start with the keyword
{0} or {1}. Then you should write {2} if you are using a regular expression and
then the expression or the path.
scope: Scope
settingsUpdated: Settings updated!
user: User
userCommands: Commands
userCommandsHelp:
'A space separated list with the available commands for this user.
Example:'
userCreated: User created!
userDeleted: User deleted!
userManagement: User Management
username: Username
users: Users
userUpdated: User updated!
sidebar:
help: Help
logout: Logout
myFiles: My files
newFile: New file
newFolder: New folder
servedWith: Served with
settings: Settings
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
pt: Portuguese
zhCN: Chinese (Simplified)
time:
unit: Time Unit
seconds: Seconds
minutes: Minutes
hours: Hours
days: Days

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

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

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

@@ -0,0 +1,198 @@
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
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: Inglês
pt: Português
zhCN: Chinês (Simplificado)
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
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
servedWith: Servido com
settings: Configurações
siteSettings: Configurações do Site
time:
unit: Unidades de Tempo
seconds: Segundos
minutes: Minutos
hours: Horas
days: Dias

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

@@ -0,0 +1,187 @@
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: 获取永久链接
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: 语言
newPassword: 您的新密码
newPasswordConfirm: 重输一遍新密码
newUser: 新建用户
password: 密码
passwordUpdated: 密码更新!
permissions: 权限
permissionsHelp: >
'您可以将该用户设置为管理员 或单独选择各项权限. 如果选择 "管理员(Administrator)" ,
将自动检查所有其他选项, 并且该用户可以管理其他用户.'
profileSettings: 配置文件设置
ruleExample1: >
'阻止用户访问每个文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore).'
ruleExample2: 阻止用户访问其目录范围内任何名为 Caddyfile 的文件/文件夹.
rules: 规则
rulesHelp1: >
'这里您可以为特定用户制定一组允许或不允许的规则,
阻止的文件将不会显示到列表中, 用户将无法访问, 支持相对于用户的范围.'
rulesHelp2: >
每行一条规则, 必须以关键词 {0} 或 {1} 开头. 如果使用正则表达式,
然后使用表达式或路径, 则需要在第二列单词加入 {2} .
scope: 目录范围
settingsUpdated: 设置更新!
user: 用户
userCommands: 用户命令(Linux 代码)
userCommandsHelp: '一个以空格分割的列表, 用于指定该用户可以执行的命令(Linux 代码), 例如:'
userCreated: 用户创建!
userDeleted: 用户删除!
userManagement: 用户管理
username: 用户名
users: 用户
userUpdated: 用户更新!
sidebar:
help: 帮助
logout: 注销
myFiles: 我的文件
newFile: 新建文件
newFolder: 新建文件夹
servedWith: 服务提供
settings: 设置
siteSettings: 网站设置
hugoNew: Hugo New
preview: 预览
search:
images: 图像
music: 音乐
pdf: PDF
pressToExecute: 按 Enter 键(回车)执行.
pressToSearch: 按 Enter 键(回车)进行搜索.
search: 搜索...
searchOrCommand: 搜索或者执行命令(Linux 代码)...
searchOrSupportedCommand: '搜索或使用您支持使用的命令(一次只能执行一个命令):'
type: 键入并按 Enter 键(回车)进行搜索.
types: 类型
video: 视频
writeToSearch: 请输入要搜索的内容
languages:
en: English
pt: Portuguese
zhCN: Chinese (Simplified)
time:
unit: 时间单位
seconds:
minutes: 分钟
hours: 小时
days:

View File

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

View File

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

View File

@@ -8,19 +8,21 @@ Vue.use(Vuex)
const state = {
user: {},
req: {},
plugins: window.plugins || [],
clipboard: {
key: '',
items: []
},
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
jwt: '',
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,17 @@ 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
},
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
@@ -48,6 +58,9 @@ const mutations = {
resetClipboard: (state) => {
state.clipboard.key = ''
state.clipboard.items = []
},
setSchedule: (state, value) => {
state.schedule = value
}
}

View File

@@ -2,15 +2,17 @@ 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) => {
@@ -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,7 +35,7 @@ function fetch (url) {
})
}
function rm (url) {
export function remove (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -57,7 +56,7 @@ function rm (url) {
})
}
function post (url, content = '') {
export function post (url, content = '', overwrite = false) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -65,26 +64,39 @@ function post (url, content = '') {
request.open('POST', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
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}`)
request.setRequestHeader('Publish', publish)
if (date !== '') {
request.setRequestHeader('Schedule', date)
}
request.onload = () => {
if (request.status === 200) {
@@ -132,15 +144,15 @@ function moveCopy (items, copy = false) {
return Promise.all(promises)
}
function move (items) {
export function move (items) {
return moveCopy(items)
}
function copy (items) {
export function copy (items) {
return moveCopy(items, true)
}
function checksum (url, algo) {
export function checksum (url, algo) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -160,7 +172,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 +183,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 +194,7 @@ function search (url, search, onmessage, onclose) {
conn.onclose = onclose
}
function download (format, ...files) {
export function download (format, ...files) {
let url = `${store.state.baseURL}/api/download`
if (files.length === 1) {
@@ -206,7 +218,59 @@ function download (format, ...files) {
window.open(url)
}
function getUsers () {
export function getSettings () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/settings/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve(JSON.parse(request.responseText))
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
export function updateSettings (param, which) {
return new Promise((resolve, reject) => {
let data = {
what: 'settings',
which: which,
data: {}
}
data.data[which] = param
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
switch (request.status) {
case 200:
resolve()
break
default:
reject(request.responseText)
break
}
}
request.onerror = (error) => { reject(error) }
request.send(JSON.stringify(data))
})
}
// USERS
export function getUsers () {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/`, true)
@@ -227,7 +291,7 @@ function getUsers () {
})
}
function getUser (id) {
export function getUser (id) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
@@ -248,7 +312,7 @@ function getUser (id) {
})
}
function newUser (user) {
export function newUser (user) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/users/`, true)
@@ -265,11 +329,15 @@ function newUser (user) {
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify(user))
request.send(JSON.stringify({
what: 'user',
which: 'new',
data: user
}))
})
}
function updateUser (user) {
export function updateUser (user, which) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
@@ -286,11 +354,15 @@ function updateUser (user) {
}
}
request.onerror = (error) => reject(error)
request.send(JSON.stringify(user))
request.send(JSON.stringify({
what: 'user',
which: (typeof which === 'string') ? which : 'all',
data: user
}))
})
}
function deleteUser (id) {
export function deleteUser (id) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
@@ -311,153 +383,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.open('GET', `${store.state.baseURL}/api/share${url}`, true)
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.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
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.open('POST', url, true)
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

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
<template>
<div id="login">
<form @submit="submit">
<img src="../assets/logo.svg" alt="File Manager">
<h1>File Manager</h1>
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
<input type="text" v-model="username" :placeholder="$t('login.username')">
<input type="password" v-model="password" :placeholder="$t('login.password')">
<input type="submit" :value="$t('login.submit')">
</form>
</div>
</template>
<script>
import auth from '@/utils/auth'
export default {
name: 'login',
data: function () {
return {
wrong: false,
username: '',
password: ''
}
},
methods: {
submit: function (event) {
event.preventDefault()
event.stopPropagation()
let redirect = this.$route.query.redirect
if (redirect === '' || redirect === undefined || redirect === null) {
redirect = '/files/'
}
auth.login(this.username, this.password)
.then(() => { this.$router.push({ path: redirect }) })
.catch(() => { this.wrong = true })
}
}
}
</script>

View File

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

View File

@@ -1,58 +1,63 @@
<template>
<div>
<form @submit="save" class="dashboard">
<h1 v-if="id === 0">New User</h1>
<h1 v-else>User {{ username }}</h1>
<ul id="nav">
<li>
<router-link to="/users">
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.userManagement') }}
</router-link>
</li>
<li></li>
</ul>
<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>
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
<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><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>
<h3>Commands</h3>
<h2>{{ $t('settings.permissions') }}</h2>
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
<p class="small">A space separated list with the available commands for this user. Example: <i>git svn hg</i>.</p>
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
<p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
<h3>{{ $t('settings.userCommands') }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
<input type="text" v-model.trim="commands">
<h2>Rules</h2>
<h2>{{ $t('settings.rules') }}</h2>
<p class="small">Here you can define a set of allow and disallow rules for this specific user. The blocked files won't
show up in the listings and they won't be accessible to the user. We support regex and paths relative to
the user's scope.</p>
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
<p class="small">Each rule goes in one different line and must start with the keyword <code>allow</code> or <code>disallow</code>.
Then you should write <code>regex</code> if you are using a regular expression and then the expression or the path.</p>
<i18n path="settings.rulesHelp2" tag="p" class="small">
<code>allow</code><code>disallow</code><code>regex</code>
</i18n>
<p class="small"><strong>Examples</strong></p>
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
<ul class="small">
<li><code>disallow regex \\/\\..+</code> - prevents the access to any dot file (such as .git, .gitignore) in every folder.</li>
<li><code>disallow /Caddyfile</code> - blocks the access to the file named <i>Caddyfile</i> on the root of the scope</li>
<li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
</ul>
<textarea v-model.trim="rules"></textarea>
<h2>Custom Stylesheet</h2>
<h2>{{ $t('settings.customStylesheet') }}</h2>
<textarea name="css"></textarea>
<p>
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete">Delete</button>
<input type="submit" value="Save">
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
<input type="submit" :value="$t('buttons.save')">
</p>
</form>
@@ -60,8 +65,13 @@
<h3>Delete User</h3>
<p>Are you sure you want to delete this user?</p>
<div>
<button @click="deleteUser" autofocus>Delete</button>
<button @click="closeHovers" class="cancel">Cancel</button>
<button @click="deleteUser" autofocus>{{ $t('buttons.delete') }}</button>
<button class="cancel"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">
{{ $t('buttons.cancel') }}
</button>
</div>
</div>
</div>
@@ -69,10 +79,12 @@
<script>
import { mapMutations } from 'vuex'
import api from '@/utils/api'
import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
import Languages from '@/components/Languages'
export default {
name: 'user',
components: { Languages },
data: () => {
return {
id: 0,
@@ -80,11 +92,13 @@ export default {
allowNew: false,
allowEdit: false,
allowCommands: false,
allowPublish: false,
permissions: {},
password: '',
username: '',
filesystem: '',
rules: '',
locale: '',
css: '',
commands: ''
}
@@ -92,7 +106,7 @@ export default {
computed: {
passwordPlaceholder () {
if (this.$route.path === '/users/new') return ''
return '(leave blank to avoid changes)'
return this.$t('settings.avoidChanges')
}
},
created () {
@@ -105,6 +119,7 @@ export default {
this.allowCommands = true
this.allowEdit = true
this.allowNew = true
this.allowPublish = true
for (let key in this.permissions) {
this.permissions[key] = true
}
@@ -119,17 +134,19 @@ export default {
user = 'base'
}
api.getUser(user).then(user => {
getUser(user).then(user => {
this.id = user.ID
this.admin = user.admin
this.allowCommands = user.allowCommands
this.allowNew = user.allowNew
this.allowEdit = user.allowEdit
this.allowPublish = user.allowPublish
this.filesystem = user.filesystem
this.username = user.username
this.commands = user.commands.join(' ')
this.css = user.css
this.permissions = user.permissions
this.locale = user.locale
for (let rule of user.rules) {
if (rule.allow) {
@@ -167,12 +184,14 @@ export default {
this.admin = false
this.allowNew = false
this.allowEdit = false
this.allowPublish = false
this.permissins = {}
this.allowCommands = false
this.password = ''
this.username = ''
this.filesystem = ''
this.rules = ''
this.locale = ''
this.css = ''
this.commands = ''
},
@@ -182,9 +201,9 @@ export default {
deleteUser (event) {
event.preventDefault()
api.deleteUser(this.id).then(location => {
deleteUser(this.id).then(location => {
this.$router.push({ path: '/users' })
this.$store.commit('showSuccess', 'User deleted!')
this.$store.commit('showSuccess', this.$t('settings.userDeleted'))
}).catch(e => {
this.$store.commit('showError', e)
})
@@ -194,9 +213,9 @@ export default {
let user = this.parseForm()
if (this.$route.path === '/users/new') {
api.newUser(user).then(location => {
newUser(user).then(location => {
this.$router.push({ path: location })
this.$store.commit('showSuccess', 'User created!')
this.$store.commit('showSuccess', this.$t('settings.userCreated'))
}).catch(e => {
this.$store.commit('showError', e)
})
@@ -204,8 +223,12 @@ export default {
return
}
api.updateUser(user).then(location => {
this.$store.commit('showSuccess', 'User updated!')
updateUser(user).then(location => {
if (user.ID === this.$store.state.user.ID) {
this.$store.commit('setUser', user)
}
this.$store.commit('showSuccess', this.$t('settings.userUpdated'))
}).catch(e => {
this.$store.commit('showError', e)
})
@@ -220,8 +243,10 @@ export default {
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: []
}
@@ -269,7 +294,3 @@ export default {
}
}
</script>
<style>
</style>

View File

@@ -1,12 +1,21 @@
<template>
<div class="dashboard">
<h1>Users <router-link to="/users/new"><button>New</button></router-link></h1>
<ul id="nav">
<li>
<router-link to="/settings/global">
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.globalSettings') }}
</router-link>
</li>
<li></li>
</ul>
<h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1>
<table>
<tr>
<th>Username</th>
<th>Admin</th>
<th>Scope</th>
<th>{{ $t('settings.username') }}</th>
<th>{{ $t('settings.admin') }}</th>
<th>{{ $t('settings.scope') }}</th>
<th></th>
</tr>
@@ -22,7 +31,7 @@
</template>
<script>
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'users',

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

22
auth.go
View File

@@ -15,6 +15,11 @@ import (
// authHandler proccesses the authentication for the user.
func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// NoAuth instances shouldn't call this method.
if c.NoAuth {
return 0, nil
}
// Receive the credentials from the request and unmarshal them.
var cred User
if r.Body == nil {
@@ -27,7 +32,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
}
// Checks if the user exists.
u, ok := c.FM.Users[cred.Username]
u, ok := c.Users[cred.Username]
if !ok {
return http.StatusForbidden, nil
}
@@ -56,6 +61,7 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// claims is the JWT claims.
type claims struct {
User
NoAuth bool `json:"noAuth"`
jwt.StandardClaims
}
@@ -70,6 +76,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
// Builds the claims.
claims := claims{
u,
c.NoAuth,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
Issuer: "File Manager",
@@ -78,7 +85,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 +93,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
}
@@ -113,8 +120,13 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
// validateAuth is used to validate the authentication and returns the
// User if it is valid.
func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
if c.NoAuth {
c.User = c.DefaultUser
return true, c.User
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
return c.FM.key, nil
return c.key, nil
}
var claims claims
token, err := request.ParseFromRequestWithClaims(r,
@@ -127,7 +139,7 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
return false, nil
}
u, ok := c.FM.Users[claims.User.Username]
u, ok := c.Users[claims.User.Username]
if !ok {
return false, nil
}

View File

@@ -10,6 +10,7 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
. "github.com/hacdias/filemanager"
@@ -73,6 +74,7 @@ func parse(c *caddy.Controller) ([]*config, error) {
baseURL := "/"
baseScope := "."
database := ""
noAuth := false
// Get the baseURL and baseScope
args := c.RemainingArgs()
@@ -93,6 +95,17 @@ func parse(c *caddy.Controller) ([]*config, error) {
}
database = c.Val()
case "no_auth":
if !c.NextArg() {
noAuth = true
continue
}
var err error
noAuth, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
}
}
@@ -126,6 +139,7 @@ func parse(c *caddy.Controller) ([]*config, error) {
}
fm, err := New(database, User{
Locale: "en",
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
@@ -143,6 +157,7 @@ func parse(c *caddy.Controller) ([]*config, error) {
return nil, err
}
fm.NoAuth = noAuth
m := &config{FileManager: fm}
m.SetBaseURL(baseURL)
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))

View File

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

View File

@@ -7,10 +7,10 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/plugins"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
@@ -42,6 +42,7 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
directory := "."
admin := "/admin"
database := ""
noAuth := false
// Get the baseURL and baseScope
args := c.RemainingArgs()
@@ -62,6 +63,17 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
}
database = c.Val()
case "no_auth":
if !c.NextArg() {
noAuth = true
continue
}
var err error
noAuth, err = strconv.ParseBool(c.Val())
if err != nil {
return nil, err
}
}
}
@@ -95,10 +107,11 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
}
m, err := filemanager.New(database, filemanager.User{
Locale: "en",
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
Permissions: map[string]bool{},
AllowPublish: true,
Commands: []string{"git", "svn", "hg"},
Rules: []*filemanager.Rule{{
Regex: true,
@@ -114,24 +127,20 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
}
// Initialize the default settings for Hugo.
hugo := &plugins.Hugo{
hugo := &filemanager.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)
err = m.EnableStaticGen(hugo)
if err != nil {
return nil, err
}
m.NoAuth = noAuth
m.SetBaseURL(admin)
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
configs = append(configs, m)

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

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

View File

@@ -12,8 +12,6 @@ import (
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/hacdias/filemanager/plugins"
"github.com/hacdias/filemanager"
"github.com/hacdias/fileutils"
flag "github.com/spf13/pflag"
@@ -27,11 +25,16 @@ var (
scope string
commands string
logfile string
plugin string
staticgen string
locale string
port int
noAuth bool
allowCommands bool
allowEdit bool
allowNew bool
allowPublish bool
showVer bool
version = "master"
)
func init() {
@@ -44,8 +47,12 @@ func init() {
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users")
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
flag.BoolVar(&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(&staticgen, "staticgen", "", "Static Generator you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
}
func setupViper() {
@@ -58,7 +65,10 @@ 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.BindPFlag("Port", flag.Lookup("port"))
viper.BindPFlag("Address", flag.Lookup("address"))
@@ -69,16 +79,36 @@ 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.SetConfigName("filemanager")
viper.AddConfigPath(".")
}
func printVersion() {
version = strings.TrimSpace(version)
if version == "" {
fmt.Println("filemanager is at an untracked version")
} else {
version = strings.TrimPrefix(version, "v")
fmt.Println("filemanager version", version)
}
os.Exit(0)
}
func main() {
setupViper()
flag.Parse()
if showVer {
printVersion()
}
// Add a configuration file if set.
if config != "" {
ext := filepath.Ext(config)
@@ -123,31 +153,43 @@ func main() {
AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
FileSystem: fileutils.Dir(viper.GetString("Scope")),
})
if viper.GetBool("NoAuth") {
fm.NoAuth = true
}
if err != nil {
log.Fatal(err)
}
if viper.GetString("Plugin") == "hugo" {
// Initialize the default settings for Hugo.
hugo := &plugins.Hugo{
switch viper.GetString("StaticGen") {
case "hugo":
hugo := &filemanager.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 {
if err = fm.EnableStaticGen(hugo); err != nil {
log.Fatal(err)
}
case "jekyll":
jekyll := &filemanager.Jekyll{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"},
CleanPublic: true,
}
if err = fm.ActivatePlugin("hugo", hugo); err != nil {
if err = fm.EnableStaticGen(jekyll); err != nil {
log.Fatal(err)
}
}

View File

@@ -20,14 +20,14 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// If the file isn't a directory, serve it using http.ServeFile. We display it
// inline if it is requested.
if !c.FI.IsDir {
if !c.File.IsDir {
if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline")
} else {
w.Header().Set("Content-Disposition", "attachment; filename="+c.FI.Name)
w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name)
}
http.ServeFile(w, r, c.FI.Path)
http.ServeFile(w, r, c.File.Path)
return 0, nil
}
@@ -46,14 +46,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 +93,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Defines the file name.
name := c.FI.Name
name := c.File.Name
if name == "." || name == "" {
name = "download"
}

View File

@@ -110,7 +110,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
func (i *file) getListing(c *RequestContext, r *http.Request) error {
// Gets the directory information using the Virtual File System of
// the user configuration.
f, err := c.User.FileSystem.OpenFile(c.FI.VirtualPath, os.O_RDONLY, 0)
f, err := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0)
if err != nil {
return err
}

View File

@@ -62,19 +62,24 @@ import (
"reflect"
"regexp"
"strings"
"time"
rice "github.com/GeertJohan/go.rice"
"github.com/asdine/storm"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/robfig/cron"
)
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{}
errUserExist = errors.New("user already exists")
errUserNotExist = errors.New("user does not exist")
errEmptyRequest = errors.New("request body is empty")
errEmptyPassword = errors.New("password is empty")
errEmptyUsername = errors.New("username is empty")
errEmptyScope = errors.New("scope is empty")
errWrongDataType = errors.New("wrong data type")
errInvalidUpdateField = errors.New("invalid field to update")
)
// FileManager is a file manager instance. It should be creating using the
@@ -89,6 +94,9 @@ type FileManager struct {
// The static assets.
assets *rice.Box
// Job cron.
cron *cron.Cron
// 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
// such as in caddy-filemanager plugin. It is only useful in certain situations.
@@ -99,6 +107,15 @@ 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
// staticgen is the name of the current static website generator.
staticgen string
// StaticGen is the static websit generator handler.
StaticGen StaticGen
// The Default User needed to build the New User page.
DefaultUser *User
@@ -107,9 +124,6 @@ type FileManager struct {
// 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{}
}
// Command is a command function.
@@ -139,11 +153,14 @@ type User struct {
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
Permissions map[string]bool `json:"permissions"` // Permissions added by plugins
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"`
@@ -170,44 +187,17 @@ type Regexp struct {
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{},
AllowPublish: true,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
FileSystem: fileutils.Dir("."),
}
@@ -219,9 +209,9 @@ func New(database string, base User) (*FileManager, 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"),
Users: map[string]*User{},
cron: cron.New(),
assets: rice.MustFindBox("./assets/dist"),
}
// Tries to open a database on the location provided. This
@@ -255,8 +245,10 @@ func New(database string, base User) (*FileManager, error) {
err = db.Get("config", "commands", &m.Commands)
if err != nil && err == storm.ErrNotFound {
m.Commands = map[string][]string{
"before_save": {},
"after_save": {},
"before_save": {},
"after_save": {},
"before_publish": {},
"after_publish": {},
}
err = db.Set("config", "commands", m.Commands)
}
@@ -294,6 +286,7 @@ 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 {
@@ -310,6 +303,10 @@ func New(database string, base User) (*FileManager, error) {
base.Username = ""
base.Password = ""
m.DefaultUser = &base
m.cron.AddFunc("@hourly", m.shareCleaner)
m.cron.Start()
return m, nil
}
@@ -337,111 +334,107 @@ 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)
}
if err != nil {
return err
}
// Register the command event hooks.
for _, evt := range plugin.CommandEvents {
if _, ok := m.Commands[evt]; ok {
continue
}
m.Commands[evt] = []string{}
}
err = m.db.Set("config", "commands", m.Commands)
if err != nil {
return err
}
// Register the user permissions.
for _, perm := range plugin.Permissions {
err = m.registerPermission(perm.Name, perm.Value)
if err != nil {
return err
}
}
m.Plugins[name] = options
return nil
}
// 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.
// ServeHTTP handles the request.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code, err := serveHTTP(&RequestContext{
FM: m,
User: nil,
FI: nil,
FileManager: m,
User: nil,
File: nil,
}, w, r)
if code != 0 {
if code >= 400 {
w.WriteHeader(code)
if err != nil {
log.Print(err)
w.Write([]byte(err.Error()))
} else {
log.Print(code)
w.Write([]byte(http.StatusText(code)))
if err == nil {
txt := http.StatusText(code)
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
w.Write([]byte(txt))
}
}
if err != nil {
log.Print(err)
w.Write([]byte(err.Error()))
}
}
// EnableStaticGen attaches a static generator to the current File Manager
// instance.
func (m *FileManager) EnableStaticGen(data StaticGen) error {
if reflect.TypeOf(data).Kind() != reflect.Ptr {
return errors.New("data should be a pointer to interface, not interface")
}
if h, ok := data.(*Hugo); ok {
return m.enableHugo(h)
}
if j, ok := data.(*Jekyll); ok {
return m.enableJekyll(j)
}
return errors.New("unknown static website generator")
}
func (m *FileManager) enableHugo(h *Hugo) error {
if err := h.find(); err != nil {
return err
}
m.staticgen = "hugo"
m.StaticGen = h
err := m.db.Get("staticgen", "hugo", h)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "hugo", *h)
}
return nil
}
func (m *FileManager) enableJekyll(j *Jekyll) error {
if err := j.find(); err != nil {
return err
}
if len(j.Args) == 0 {
j.Args = []string{"build"}
}
if j.Args[0] != "build" {
j.Args = append([]string{"build"}, j.Args...)
}
m.staticgen = "jekyll"
m.StaticGen = j
err := m.db.Get("staticgen", "jekyll", j)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "jekyll", *j)
}
return nil
}
// shareCleaner removes sharing links that are no longer active.
// This function is set to run periodically.
func (m FileManager) shareCleaner() {
var links []shareLink
// Get all links.
err := m.db.All(&links)
if err != nil {
log.Print(err)
return
}
// Find the expired ones.
for i := range links {
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
err = m.db.DeleteStruct(&links[i])
if err != nil {
log.Print(err)
}
}
}
}

145
http.go
View File

@@ -6,13 +6,16 @@ import (
"net/http"
"os"
"strings"
"time"
"github.com/asdine/storm"
)
// RequestContext contains the needed information to make handlers work.
type RequestContext struct {
*FileManager
User *User
FM *FileManager
FI *file
File *file
// On API handlers, Router is the APi handler we want.
Router string
}
@@ -21,9 +24,9 @@ type RequestContext struct {
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
// returns a 404 error because we're not supposed to be here!
p := strings.TrimPrefix(r.URL.Path, c.FM.BaseURL)
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
if len(p) >= len(r.URL.Path) && c.FM.BaseURL != "" {
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
return http.StatusNotFound, nil
}
@@ -33,10 +36,9 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
// 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"),
c, w,
c.assets.MustString("sw.js"),
"application/javascript",
c,
)
}
@@ -58,31 +60,41 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
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/") && c.StaticGen != nil {
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(
w,
c.FM.assets.MustString("index.html"),
c, w,
c.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)
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil
}
return renderFile(
w,
c.FM.assets.MustString("static/manifest.json"),
c, w,
c.assets.MustString("static/manifest.json"),
"application/json",
c,
)
}
@@ -107,8 +119,15 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return http.StatusForbidden, nil
}
for p := range c.FM.Plugins {
code, err := plugins[p].Handler.Before(c, w, r)
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
}
@@ -116,7 +135,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
if c.Router == "checksum" || c.Router == "download" {
var err error
c.FI, err = getInfo(r.URL, c.FM, c.User)
c.File, err = getInfo(r.URL, c.FileManager, c.User)
if err != nil {
return errorToHTTP(err, false), err
}
@@ -138,25 +157,14 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
code, err = resourceHandler(c, w, r)
case "users":
code, err = usersHandler(c, w, r)
case "commands":
code, err = commandsHandler(c, w, r)
case "plugins":
code, err = pluginsHandler(c, w, r)
case "settings":
code, err = settingsHandler(c, w, r)
case "share":
code, err = shareHandler(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
}
@@ -164,7 +172,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo")
val, err := c.FI.Checksum(query)
val, err := c.File.Checksum(query)
if err == errInvalidOption {
return http.StatusBadRequest, err
} else if err != nil {
@@ -193,18 +201,13 @@ func splitURL(path string) (string, string) {
}
// renderFile renders a file using a template with some needed variables.
func renderFile(w http.ResponseWriter, file string, contentType string, c *RequestContext) (int, error) {
func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentType string) (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),
"BaseURL": c.RootURL(),
"StaticGen": c.staticgen,
})
if err != nil {
return http.StatusInternalServerError, err
@@ -213,6 +216,66 @@ func renderFile(w http.ResponseWriter, file string, contentType string, c *Reque
return 0, nil
}
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", r.URL.Path, &s)
if err == storm.ErrNotFound {
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
}
if err != nil {
return http.StatusInternalServerError, err
}
if s.Expires && s.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&s)
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
}
r.URL.Path = s.Path
info, err := os.Stat(s.Path)
if err != nil {
return errorToHTTP(err, false), err
}
c.File = &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)

View File

@@ -9,58 +9,63 @@
"lint": "eslint --ext .js,.vue assets/src"
},
"dependencies": {
"clipboard": "^1.7.1",
"codemirror": "^5.27.4",
"filesize": "^3.5.10",
"moment": "^2.18.1",
"normalize.css": "^7.0.0",
"vue": "^2.3.3",
"vue-i18n": "^7.1.0",
"vue-router": "^2.7.0",
"vuex": "^2.3.1"
},
"devDependencies": {
"autoprefixer": "^6.7.2",
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-loader": "^7.1.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^1.1.3",
"chalk": "^2.0.1",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"eslint": "^3.19.0",
"eslint-config-standard": "^6.2.1",
"eslint-friendly-formatter": "^2.0.7",
"eslint": "^4.3.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-html": "^3.1.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.1.1",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^2.0.1",
"eslint-plugin-standard": "^3.0.1",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"extract-text-webpack-plugin": "^2.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"opn": "^5.1.0",
"optimize-css-assets-webpack-plugin": "^3.0.0",
"ora": "^1.2.0",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"sw-precache-webpack-plugin": "^0.9.1",
"sw-precache-webpack-plugin": "^0.11.4",
"uglify-js": "^3.0.23",
"url-loader": "^0.5.8",
"vue-loader": "^12.1.0",
"vue-loader": "^13.0.2",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.3.3",
"webpack": "^2.6.1",
"webpack": "^3.4.1",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0"
"webpack-merge": "^4.1.0",
"yml-loader": "^2.1.0"
},
"engines": {
"node": ">= 4.0.0",

View File

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

View File

@@ -1,227 +0,0 @@
package plugins
const hugoJavaScript = `'use strict';
(function () {
if (window.plugins === undefined || window.plugins === null) {
window.plugins = []
}
let regenerate = function (data, url) {
url = data.api.removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', data.store.state.baseURL + "/api/hugo" + url, true)
request.setRequestHeader('Authorization', "Bearer " + data.store.state.jwt)
request.setRequestHeader('Regenerate', 'true')
request.onload = () => {
if (request.status === 200) {
resolve()
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
let newArchetype = function (data, url, type) {
url = data.api.removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', data.store.state.baseURL + "/api/hugo" + url, true)
request.setRequestHeader('Authorization',"Bearer " + data.store.state.jwt)
request.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()
})
}
let schedule = function (data, file, date) {
file = data.api.removePrefix(file)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', data.store.state.baseURL + "/api/hugo" + file, true)
request.setRequestHeader('Authorization', "Bearer " + data.store.state.jwt)
request.setRequestHeader('Schedule', date)
request.onload = () => {
if (request.status === 200) {
resolve(request.getResponseHeader('Location'))
} else {
reject(request.responseText)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
window.plugins.push({
name: 'hugo',
credits: 'With a flavour of <a rel="noopener noreferrer" href="https://github.com/hacdias/caddy-hugo">Hugo</a>.',
header: {
visible: [
{
if: function (data, route) {
return (data.store.state.req.kind === 'editor' &&
!data.store.state.loading &&
data.store.state.user.allowEdit &
data.store.state.user.permissions.allowPublish)
},
click: function (event, data, route) {
event.preventDefault()
document.getElementById('save-button').click()
// TODO: wait for save to finish?
data.buttons.loading('publish')
regenerate(data, route.path)
.then(() => {
data.buttons.done('publish')
data.store.commit('showSuccess', 'Post published!')
data.store.commit('setReload', true)
})
.catch((error) => {
data.buttons.done('publish')
data.store.commit('showError', error)
})
},
id: 'publish-button',
icon: 'send',
name: 'Publish'
}
],
hidden: [
{
if: function (data, route) {
return (data.store.state.req.kind === 'editor' &&
!data.store.state.loading &&
data.store.state.req.metadata !== undefined &&
data.store.state.req.metadata !== null &&
data.store.state.user.permissions.allowPublish)
},
click: function (event, data, route) {
document.getElementById('save-button').click()
data.store.commit('showHover', 'schedule')
},
id: 'schedule-button',
icon: 'alarm',
name: 'Schedule'
}
]
},
sidebar: [
{
click: function (event, data, route) {
data.router.push({ path: '/files/settings' })
},
icon: 'settings',
name: 'Hugo Settings'
},
{
click: function (event, data, route) {
data.store.commit('showHover', 'new-archetype')
},
if: function (data, route) {
return data.store.state.user.allowNew
},
icon: 'merge_type',
name: 'Hugo New'
} /* ,
{
click: function (event, data, route) {
console.log('evt')
},
icon: 'remove_red_eye',
name: 'Preview'
} */
],
prompts: [
{
name: 'new-archetype',
title: 'New file',
description: 'Create a new post based on an archetype. Your file will be created on content folder.',
inputs: [
{
type: 'text',
name: 'file',
placeholder: 'File name'
},
{
type: 'text',
name: 'archetype',
placeholder: 'Archetype'
}
],
ok: 'Create',
submit: function (event, data, route) {
event.preventDefault()
let file = event.currentTarget.querySelector('[name="file"]').value
let type = event.currentTarget.querySelector('[name="archetype"]').value
if (type === '') type = 'default'
data.store.commit('closeHovers')
newArchetype(data, '/' + file, type)
.then((url) => {
data.router.push({ path: url })
})
.catch(error => {
data.store.commit('showError', error)
})
}
},
{
name: 'schedule',
title: 'Schedule',
description: 'Pick a date and time to schedule the publication of this post.',
inputs: [
{
type: 'datetime-local',
name: 'date',
placeholder: 'Date'
}
],
ok: 'Schedule',
submit: function (event, data, route) {
event.preventDefault()
data.buttons.loading('schedule')
let date = event.currentTarget.querySelector('[name="date"]').value
if (date === '') {
data.buttons.done('schedule')
data.store.commit('showError', 'The date must not be empty.')
return
}
schedule(data, route.path, date)
.then(() => {
data.buttons.done('schedule')
data.store.commit('showSuccess', 'Post scheduled!')
})
.catch((error) => {
data.buttons.done('schedule')
data.store.commit('showError', error)
})
}
}
]
})
})()`

View File

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

View File

@@ -4,11 +4,14 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/hacdias/fileutils"
)
@@ -34,7 +37,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
case http.MethodPut:
// Before save command handler.
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
if err := c.FM.Runner("before_save", path); err != nil {
if err := c.Runner("before_save", path); err != nil {
return http.StatusInternalServerError, err
}
@@ -44,7 +47,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// After save command handler.
if err := c.FM.Runner("after_save", path); err != nil {
if err := c.Runner("after_save", path); err != nil {
return http.StatusInternalServerError, err
}
@@ -60,7 +63,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file.
f, err := getInfo(r.URL, c.FM, c.User)
f, err := getInfo(r.URL, c.FileManager, c.User)
if err != nil {
return errorToHTTP(err, false), err
}
@@ -73,7 +76,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
// If it is a dir, go and serve the listing.
if f.IsDir {
c.FI = f
c.File = f
return listingHandler(c, w, r)
}
@@ -101,7 +104,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.FI
f := c.File
f.Kind = "listing"
// Tries to get the listing data.
@@ -112,7 +115,7 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
listing := f.listing
// Defines the cookie scope.
cookieScope := c.FM.RootURL()
cookieScope := c.RootURL()
if cookieScope == "" {
cookieScope = "/"
}
@@ -155,6 +158,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,21 +173,21 @@ 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)
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")
}
}
// 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
}
@@ -196,12 +205,73 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
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)
return http.StatusOK, nil
}
func resourcePublishSchedule(c *RequestContext, 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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// Before save command handler.
if err := c.Runner("before_publish", path); 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); 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) {
if !c.User.AllowEdit {

View File

@@ -1 +1 @@
261f3077300416639c340ca30538b4c40284dd00
a82ac66ecfe8db47bc49c927163556011cedc538

View File

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

137
share.go Normal file
View File

@@ -0,0 +1,137 @@
package filemanager
import (
"encoding/hex"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/asdine/storm"
"github.com/asdine/storm/q"
)
type shareLink struct {
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
Expires bool `json:"expires"`
ExpireDate time.Time `json:"expireDate"`
}
func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
case http.MethodGet:
return shareGetHandler(c, w, r)
case http.MethodDelete:
return shareDeleteHandler(c, w, r)
case http.MethodPost:
return sharePostHandler(c, w, r)
}
return http.StatusNotImplemented, nil
}
func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var (
s []*shareLink
path = filepath.Join(string(c.User.FileSystem), r.URL.Path)
)
err := c.db.Find("Path", path, &s)
if err == storm.ErrNotFound {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
for i, link := range s {
if link.Expires && link.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&shareLink{Hash: link.Hash})
s = append(s[:i], s[i+1:]...)
}
}
return renderJSON(w, s)
}
func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
var s shareLink
expire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit")
if expire == "" {
err := c.db.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&s)
if err == nil {
w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
return 0, nil
}
}
bytes, err := generateRandomBytes(32)
if err != nil {
return http.StatusInternalServerError, err
}
str := hex.EncodeToString(bytes)
s = shareLink{
Path: path,
Hash: str,
Expires: expire != "",
}
if expire != "" {
num, err := strconv.Atoi(expire)
if err != nil {
return http.StatusInternalServerError, err
}
var add time.Duration
switch unit {
case "seconds":
add = time.Second * time.Duration(num)
case "minutes":
add = time.Minute * time.Duration(num)
case "days":
add = time.Hour * 24 * time.Duration(num)
default:
add = time.Hour * time.Duration(num)
}
s.ExpireDate = time.Now().Add(add)
}
err = c.db.Save(&s)
if err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, s)
}
func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)
if err == storm.ErrNotFound {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
err = c.db.DeleteStruct(&s)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}

307
staticgen.go Normal file
View File

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

249
users.go
View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"net/http"
"os"
"sort"
"strconv"
"strings"
@@ -11,20 +12,22 @@ import (
"github.com/asdine/storm"
)
type modifyRequest struct {
What string `json:"what"` // Answer to: what data type?
Which string `json:"which"` // Answer to: which field?
}
type modifyUserRequest struct {
*modifyRequest
Data *User `json:"data"`
}
// usersHandler is the entry point of the users API. It's just a router
// to send the request to its
func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/change-password" {
return usersUpdatePassword(c, w, r)
}
if r.URL.Path == "/change-css" {
return usersUpdateCSS(c, w, r)
}
// If the user is admin and the HTTP Method is not
// PUT, then we return forbidden.
if !c.User.Admin {
// If the user isn't admin and isn't making a PUT
// request, then return forbidden.
if !c.User.Admin && r.Method != http.MethodPut {
return http.StatusForbidden, nil
}
@@ -61,32 +64,38 @@ func getUserID(r *http.Request) (int, error) {
// getUser returns the user which is present in the request
// body. If the body is empty or the JSON is invalid, it
// returns an error.
func getUser(r *http.Request) (*User, error) {
func getUser(r *http.Request) (*User, string, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, errEmptyRequest
return nil, "", errEmptyRequest
}
u := &User{}
err := json.NewDecoder(r.Body).Decode(u)
// Parses the request body and checks if it's well formed.
mod := &modifyUserRequest{}
err := json.NewDecoder(r.Body).Decode(mod)
if err != nil {
return nil, err
return nil, "", err
}
return u, nil
// Checks if the request type is right.
if mod.What != "user" {
return nil, "", errWrongDataType
}
return mod.Data, mod.Which, nil
}
func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Request for the default user data.
if r.URL.Path == "/base" {
return renderJSON(w, c.FM.DefaultUser)
return renderJSON(w, c.DefaultUser)
}
// Request for the listing of users.
if r.URL.Path == "/" {
users := []User{}
for _, user := range c.FM.Users {
for _, user := range c.Users {
// Copies the user info and removes its
// password so it won't be sent to the
// front-end.
@@ -108,7 +117,7 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Searches for the user and prints the one who matches.
for _, user := range c.FM.Users {
for _, user := range c.Users {
if user.ID != id {
continue
}
@@ -127,11 +136,26 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusMethodNotAllowed, nil
}
u, err := getUser(r)
u, _, err := getUser(r)
if err != nil {
return http.StatusBadRequest, err
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername
}
// Checks if filesystem isn't empty.
if u.FileSystem == "" {
return http.StatusBadRequest, errEmptyScope
}
// Checks if password isn't empty.
if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword
}
// The username, password and scope cannot be empty.
if u.Username == "" || u.Password == "" || u.FileSystem == "" {
return http.StatusBadRequest, errors.New("username, password or scope is empty")
@@ -152,6 +176,11 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.ID = 0
}
// Checks if the scope exists.
if code, err := checkFS(string(u.FileSystem)); err != nil {
return code, err
}
// Hashes the password.
pw, err := hashPassword(u.Password)
if err != nil {
@@ -161,7 +190,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.Password = pw
// Saves the user to the database.
err = c.FM.db.Save(u)
err = c.db.Save(u)
if err == storm.ErrAlreadyExists {
return http.StatusConflict, errUserExist
}
@@ -171,7 +200,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Saves the user to the memory.
c.FM.Users[u.Username] = u
c.Users[u.Username] = u
// Set the Location header and return.
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
@@ -179,6 +208,29 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return 0, nil
}
func checkFS(path string) (int, error) {
info, err := os.Stat(path)
if err != nil {
if !os.IsNotExist(err) {
return http.StatusInternalServerError, err
}
err = os.MkdirAll(path, 0666)
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
if !info.IsDir() {
return http.StatusBadRequest, errors.New("Scope is not a dir")
}
return 0, nil
}
func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil
@@ -190,7 +242,7 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
// Deletes the user from the database.
err = c.FM.db.DeleteStruct(&User{ID: id})
err = c.db.DeleteStruct(&User{ID: id})
if err == storm.ErrNotFound {
return http.StatusNotFound, errUserNotExist
}
@@ -200,9 +252,9 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
// Delete the user from the in-memory users map.
for _, user := range c.FM.Users {
for _, user := range c.Users {
if user.ID == id {
delete(c.FM.Users, user.Username)
delete(c.Users, user.Username)
break
}
}
@@ -210,72 +262,84 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusOK, nil
}
func usersUpdatePassword(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != http.MethodPut {
return http.StatusMethodNotAllowed, nil
}
u, err := getUser(r)
if err != nil {
return http.StatusBadRequest, err
}
if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword
}
pw, err := hashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
c.User.Password = pw
err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func usersUpdateCSS(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != http.MethodPut {
return http.StatusMethodNotAllowed, nil
}
u, err := getUser(r)
if err != nil {
return http.StatusBadRequest, err
}
c.User.CSS = u.CSS
err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// New users should be created on /api/users.
if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil
}
// Gets the user ID from the URL and checks if it's valid.
id, err := getUserID(r)
if err != nil {
return http.StatusInternalServerError, err
}
u, err := getUser(r)
// Checks if the user has permission to access this page.
if !c.User.Admin && id != c.User.ID {
return http.StatusForbidden, nil
}
// Gets the user from the request body.
u, which, err := getUser(r)
if err != nil {
return http.StatusBadRequest, err
}
// The username and the filesystem cannot be empty.
if u.Username == "" || u.FileSystem == "" {
return http.StatusBadRequest, errors.New("Username, password or scope are empty")
// Updates the CSS and locale.
if which == "partial" {
c.User.CSS = u.CSS
c.User.Locale = u.Locale
err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
if err != nil {
return http.StatusInternalServerError, err
}
err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
// Updates the Password.
if which == "password" {
if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword
}
pw, err := hashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
c.User.Password = pw
err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
// If can only be all.
if which != "all" {
return http.StatusBadRequest, errInvalidUpdateField
}
// Checks if username isn't empty.
if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername
}
// Checks if filesystem isn't empty.
if u.FileSystem == "" {
return http.StatusBadRequest, errEmptyScope
}
// Checks if the scope exists.
if code, err := checkFS(string(u.FileSystem)); err != nil {
return code, err
}
// Initialize rules if they're not initialized.
@@ -288,48 +352,45 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.Commands = []string{}
}
var ouser *User
for _, user := range c.FM.Users {
// Gets the current saved user from the in-memory map.
var suser *User
for _, user := range c.Users {
if user.ID == id {
ouser = user
suser = user
break
}
}
if ouser == nil {
if suser == nil {
return http.StatusNotFound, nil
}
u.ID = id
if u.Password == "" {
u.Password = ouser.Password
} else {
// Changes the password if the request wants it.
if u.Password != "" {
pw, err := hashPassword(u.Password)
if err != nil {
return http.StatusInternalServerError, err
}
u.Password = pw
}
if u.Permissions == nil {
u.Permissions = c.FM.DefaultUser.Permissions
} else {
u.Password = suser.Password
}
// Updates the whole User struct because we always are supposed
// to send a new entire object.
err = c.FM.db.Save(u)
err = c.db.Save(u)
if err != nil {
return http.StatusInternalServerError, err
}
// If the user changed the username, delete the old user
// from the in-memory user map.
if ouser.Username != u.Username {
delete(c.FM.Users, ouser.Username)
if suser.Username != u.Username {
delete(c.Users, suser.Username)
}
c.FM.Users[u.Username] = u
c.Users[u.Username] = u
return http.StatusOK, nil
}

View File

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