Compare commits

...

34 Commits

Author SHA1 Message Date
Henrique Dias
a67a9779e9 bugfix
Former-commit-id: 50b2aa02d2aa5720b974618a138b966ddb2cd7c6 [formerly a4f36493c5344d5f62993f3ea4a91ebc63e519a2] [formerly 5929d2752a447f353a3d9ecb7c0fc28e4cbff253 [formerly 8c5be28206]]
Former-commit-id: 7a3fc8c63c7d60218eaeafd01a3de6547b4df574 [formerly d0af234befdabf3376ec5d761a1c9d388ad679d4]
Former-commit-id: b4ac3bad9f1f0a8dea996d08d270a3bac3481796
2017-08-12 16:45:38 +01:00
Henrique Dias
059c0dc071 build assets
Former-commit-id: 46b7aaa4c4f250a18248221fb5de4dadce75a367 [formerly dd818597d00617abc1890d5cc4936eebf84f170a] [formerly 1584baa12ace346afc0f8589f0f749c66e9d2512 [formerly 511e417f91]]
Former-commit-id: 573415b047d2832a6e206c1f4af416247aeee00f [formerly 3d614ab56d8a5acdb136e666657a7065823ae3a0]
Former-commit-id: 8116e0a59e2ac3065f6fe904be897fba355f21dc
2017-08-12 16:39:52 +01:00
Equim
89a8360c4c Add Traditional Chinese localization. (#201)
* i18n: better formatted yaml for newline matter

* i18n: add zh-tw.yaml


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

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

* i18n: use the languages themselves to describe languges


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

Progresses on #192

Progresses on #192

Little API update

Build assets


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

Address #184

build assets

Update zh-cn.yaml (#194)


Former-commit-id: 4572f93371647c0a6b53cc375ec5bb00356a37c9 [formerly e58e4793ac0ca6915b605d7b2a8a77f0aec31172] [formerly 43635f6b98f546ec0e2656d26031388aef63a902 [formerly da4fd84002]]
Former-commit-id: 15422887ad29dea63bdf861d9da8f8d28b4fbc8f [formerly 3949ffa499cf999c3f4b50ee18802c0f87a23807]
Former-commit-id: fac5ceeee3fa969239d9ef5e04ef5542a61d2761
2017-08-10 11:08:50 +01:00
Toyo
30be924648 Update zh-cn.yaml (#194)
Former-commit-id: 811ff539f580c6e5e926f7000a1ad0152c58142a [formerly 58cb75b9fc4f1022b26f6b09016ae0b5a06b7e41] [formerly 1e6fb8f5112a4eccbc469da1557bb15586fddd03 [formerly c59ac7e321]]
Former-commit-id: c7967b4572973a609997e49541628695a40143cf [formerly 331933c8f014500698e925ea6e527bf2330a8953]
Former-commit-id: ee537e70514fe5507ff985a0816f6e7ae7d06dcb
2017-08-09 08:34:28 +01:00
Henrique Dias
bfdb924cb7 Search improvements #156
Former-commit-id: 131b9e565750205e39b9b2d029bff895b4cdbe30 [formerly 1c848718a1f743c9c74a1170cb932eaf7ae8584b] [formerly b856d5d960edf7f40dfb0f15be91dc2b8cbf4c06 [formerly 9819c87853]]
Former-commit-id: 332be2031e391993df55e705250e7459f9227877 [formerly de5fcf78ac347621ff60ba89c72a3f88efdc0f5e]
Former-commit-id: 57f767bf0ea685f3f147b57b441e8cb2b048ea18
2017-08-08 11:33:10 +01:00
Henrique Dias
6d853d63ed Hugo preview mode. Update Plugin registration.
Former-commit-id: b6c33f0ae36e7caa8cf813eeb6f8fdeb554659e9 [formerly 4d62c13f0b7e5b7610a0c9037d646c2005e9ba88] [formerly 1e9ded0427d81084c92a68d5eddb8c7620fb189c [formerly ae8eaf96c4]]
Former-commit-id: 76699219a6d176777f4478eab5141bba5a613a17 [formerly bb698073213359bebce84ea196b30e9467f448c0]
Former-commit-id: 538927b5a6fad024eb062cf746068372c384b883
2017-08-08 10:21:25 +01:00
Henrique Dias
4bae90c80f Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 24a9bcd2de3e275c99da8eaa091b98865a82fd32 [formerly 596230fe6cf428f04822f5af000ddee429568c3a] [formerly fc7ec7bc4eb116e9afc04d4f7eae428052a91958 [formerly a2ed744f24]]
Former-commit-id: a45873320fb58f0abc1629341dbf97c96ed65cb9 [formerly 759add9806b1599e889133973b7e39919862accc]
Former-commit-id: 71acac2d754874a22dffd8f0b601465e2308a211
2017-08-08 09:19:11 +01:00
Henrique Dias
e464eb69ed fix #193
Former-commit-id: 5765a9c1178a015e036481e6eb78f498bead5b04 [formerly 37680ed0dab2e85b3ac921fbe3fd354a238211f5] [formerly ed9fbac269e054784e5f375c71c6423441667e82 [formerly e42ec4b42f]]
Former-commit-id: 5877cfd762b4dc1e6924ae87f136f24c82a98fdc [formerly 65376cc4c37750b9e7efdfae8fe24687a683d057]
Former-commit-id: 4063ea78f117a8e3d04a047280e8462c6d5d493e
2017-08-08 09:18:24 +01:00
Toyo
fee9330cfa Update zh-cn.yaml (#191)
Former-commit-id: e3cddc3fb1540e5308e6aca310e1fee7b7d18c9e [formerly a7b5a23715965b972e7d9ea97a6349388ca21df3] [formerly 339be0b244508d0c88a76e271e9ada949d41f3c4 [formerly dc7f389fde]]
Former-commit-id: 631ab6a08944f2e861a37aa59e2c01348a0a59f9 [formerly 96e267b39b21d6beb5693ce11c92d802512625d3]
Former-commit-id: 533b10726159061723dfce3511fb11bd4f167789
2017-08-07 18:07:21 +01:00
Henrique Dias
b034c87ec0 Merge branch 'master' of https://github.com/hacdias/filemanager
Former-commit-id: 599633a56b78c94855f0e75ec76823a5670e61db [formerly 60ef68d1eef84903835db5de79a02f5554bc9a84] [formerly b96770a982b1fcbf5c4fffa00978e535e921bff2 [formerly 40fda1368a]]
Former-commit-id: c79c723954d8a5ac26f9f7594396fd2fd740c1e4 [formerly b103a905107b1f72f7c98aaf309b591f4429fb7d]
Former-commit-id: e40b3de8dfd04947c0b037a854f04ccd81e88fa4
2017-08-07 14:48:05 +01:00
Henrique Dias
87420bcefd Build assets
Former-commit-id: fa20c1e669918db4bfd51f5d12785923392ca966 [formerly 24061bfbf3dc8f0a9f42b929d68946e4969db5f7] [formerly 3450f5d2eb248b7dfe88616c2d294cee725ccf92 [formerly 231f3f46ac]]
Former-commit-id: b215d97223440354b9bacbaf467da269839880f0 [formerly c56116a6a6e8c22092b8a0182880401ab31fe301]
Former-commit-id: ac5a0a3df55e8cb63297ade8345806671ec519f8
2017-08-07 14:48:02 +01:00
Henrique Dias
d6ca579519 Support replace feature; close #188; multiple bug fixes on upload
Former-commit-id: 30b18e418111df00903965a7e4135441cd1767a1 [formerly 60393b5e1a291d1aab2313e7dd8cd2e1e203c03b] [formerly c372bc1355db36d2d7b0bc602977616ae52a141c [formerly 6ee846ef0e]]
Former-commit-id: c6c5aa9646c78fe86af74aac00cc1c7c307ad678 [formerly 6d3accd032458203105a474b0f8204d8206c2440]
Former-commit-id: ab35a8effb0b01406bef44328ef3c45f5ea1cbfd
2017-08-07 14:44:32 +01:00
Toyo
d74c9ae5af Update zh-cn.yaml (#189)
Former-commit-id: e501daa9bb525b5695584fa5b8b99daa9571e27d [formerly de35b99396f1d39fee20aca6250cc61aeec6bcbc] [formerly 55ca4eb602d5b54dbc123cbc9772ae451a7f5ca7 [formerly c911eacc7b]]
Former-commit-id: e9ff475525798ada59ffa004ad6da46d34daa0b7 [formerly 893dce937e9fe40d314e3ed22ba0686d72e6b5aa]
Former-commit-id: 59093c53423038536ed5a7c695ce42d139ffe978
2017-08-05 10:25:33 +01:00
Henrique Dias
2f60562143 Add navigation links on users pages; close #187
Former-commit-id: 8e85fdb8938f3cb45561f0f7564f1877f8b7c1c1 [formerly 311658b86cd8c4304dfddaf04d89b06072b89245] [formerly da85ccd4c26d5aa9632884d0cc65ae259c5beeea [formerly a117f83256]]
Former-commit-id: 218b91f816d58ce0cc01cd2c7b590ac7ea5e589e [formerly f995eb799754c36567760aca8ce2455d33f6caa7]
Former-commit-id: f848927433956edda8a12083b8a53ce62d47f3ff
2017-08-05 10:06:12 +01:00
Henrique Dias
1527ca0c50 Build assets Fri Aug 4 16:38:35 GMTST 2017
Former-commit-id: 68b8596be01fba8888cbe2986f045a74974b7358 [formerly a9b16a017049aef7cc57a838de93e65bef0b98e2] [formerly ec326398476caa44b89d7afa087612888cde4181 [formerly 26d9fd98ff]]
Former-commit-id: 7abe14b5648fd8a6cbee3fdd5700433cfdde15a4 [formerly 20fe929de14f28e21bf9309c032d3f0758d8ab27]
Former-commit-id: 1640635ad843b9d1e180b6705a697b205562b209
2017-08-04 16:38:35 +01:00
Henrique Dias
0dee98b40a Add aria label; close #131
Former-commit-id: 8fc595933b73d635bbe20e018958343d9dd3e31b [formerly c056d2fa20bec942e89710ff469e12cb07a498a9] [formerly a455096212ba0cb70102a99c714d8ff3e1401f17 [formerly 1861645b04]]
Former-commit-id: 794d333ef3a2788c5e76f7c1a558af8be1137c20 [formerly ab8ca7057862e8919a7d01c3d71ecd70ba7b32f3]
Former-commit-id: 3430567351ec7425e167c4c128bb7316ff631eac
2017-08-04 16:36:27 +01:00
Henrique Dias
412ac9c9d6 Search results slightly prettier
Former-commit-id: 518b86ee61c4816e346824e36ca2b8afc5a887ae [formerly d0e4ab7a725aa0dcf35acc0965e685563d33590e] [formerly 3eeb7807675ab03f3eb3ba9a2fb0f612c1ae742d [formerly 522d50333f]]
Former-commit-id: 311dbbb9e90be32928fdca3087bae73e574610fd [formerly fbf64faddb2a189f675b4652cd170418bbbf48f7]
Former-commit-id: 834138c56639871c32ad7f393440aee6a7281a9a
2017-08-04 16:15:07 +01:00
Henrique Dias
9c2609995a close #186
Former-commit-id: 14c5044db7c81c03f27c4dbf0e7c9ba3a89cb03a [formerly 7d0fe81767e4b33c64ae7da7dd684ca8f30539c4] [formerly 815bd3a5d9770e79527de56e8d6b5d79f34cb291 [formerly 1234ee32e4]]
Former-commit-id: 2330876399f0e325dc554a08de2513b385e39a7a [formerly 638d4627e6418da2d4ebba046735bd39e8e63ae2]
Former-commit-id: d1569f07397e7f31c164080326a3a91724999778
2017-08-04 14:10:23 +01:00
Henrique Dias
68cb4ee980 Update version output
Former-commit-id: 0d8bbcef2b4cdb93fc7c4929b875eb920fc01a64 [formerly 8bf71ac7db4a94d47346bbe382b135f60bc00788] [formerly 1426e8119be5d07f0d2a759f30784a223d76208d [formerly b7887ed162]]
Former-commit-id: 3e0d9dff358868119c6d3b109896259b84a1e565 [formerly e15610b544cb000a0ce88392bd13949a578fd605]
Former-commit-id: 6497b3d602ac78be939abc70d6c337799e9d85d8
2017-08-04 13:35:21 +01:00
Henrique Dias
adc82dd85e Delete README.md
Former-commit-id: c32b7a3c7967a342832c19687420fc53176bdf46 [formerly b0dcce97677ad53314f0318819bca23646fcf770] [formerly 2f642c119a8148aa6b77e08efd0fd1b7e6d3b1a0 [formerly 004f6ef0a2]]
Former-commit-id: 07190b22fb025336b9a7cbc3cdba0f0eac32d301 [formerly b9deeb3285c5ddc978900766677a76d3b2294be5]
Former-commit-id: 97d26cdbe84585dbf3f008d7faa058579f31336f
2017-08-02 15:24:02 +01:00
Henrique Dias
4f3375ee8d NoAuth for Caddy plugins
Former-commit-id: 5b8306eb527c9f21d14736c97e67b8689e69d025 [formerly 42d1b7035277e06aecc9a812ea4d92022a36c2a6] [formerly 31905bebcc8f337bc8f6815425778bfc652f43e4 [formerly b5961b6b63]]
Former-commit-id: 44aa1bc7df6ffa633b333b75a5045c612db44aae [formerly d22761bb6f49156f7889e28f3af8edb439c7e023]
Former-commit-id: 5a8a899a97bb7ec4c9433723c7d53f87163cccac
2017-08-02 14:49:15 +01:00
69 changed files with 2699 additions and 1114 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

@@ -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">
@@ -26,8 +27,6 @@
if (file.match(/\.(js|css)$/)) { %>
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
<!-- Plugins info -->
<script>{{ .JavaScript }}</script>
<style>
#loading {
position: fixed;

View File

@@ -16,19 +16,11 @@
<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="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
@@ -37,6 +29,7 @@
<!-- 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,25 +39,16 @@
<!-- 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>
@@ -91,6 +75,8 @@ 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 * as api from '@/utils/api'
import buttons from '@/utils/buttons'
@@ -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

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

View File

@@ -17,18 +17,64 @@
<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'
}
}
},
@@ -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

@@ -17,10 +17,32 @@
</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>
<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>
<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>
@@ -38,7 +60,6 @@
<p class="credits">
<span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
<span v-for="plugin in plugins" :key="plugin.name" v-html="plugin.credits"><br></span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
</p>
</nav>
@@ -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

@@ -8,7 +8,7 @@
<script>
import {mapGetters, mapState} from 'vuex'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'download-button',

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

@@ -11,13 +11,13 @@
<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,22 +112,30 @@ 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', '')
})
}
}

View File

@@ -8,23 +8,37 @@
</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')">
<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')">
<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')">
<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>
@@ -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

@@ -38,7 +38,7 @@
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
import InfoButton from '@/components/buttons/Info'
import DeleteButton from '@/components/buttons/Delete'
import RenameButton from '@/components/buttons/Rename'

View File

@@ -6,7 +6,10 @@
<file-list @update:selected="val => dest = val"></file-list>
<div>
<button class="ok" @click="copy">{{ $t('buttons.copy') }}</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')"
@@ -18,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 {
@@ -48,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

@@ -4,7 +4,9 @@
<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>{{ $t('buttons.delete') }}</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')"
@@ -15,7 +17,7 @@
<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'
@@ -34,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 => {
@@ -57,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

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

@@ -4,8 +4,14 @@
<h3>{{ $t('prompts.error') }}</h3>
<pre>{{ $store.state.showMessage }}</pre>
<div>
<button @click="close" autofocus>{{ $t('buttons.close') }}</button>
<button @click="reportIssue" class="cancel">{{ $t('buttons.reportIssue') }}</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,6 +4,9 @@
<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>
@@ -16,7 +19,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: 'file-list',

View File

@@ -15,7 +15,11 @@
</ul>
<div>
<button type="submit" @click="$store.commit('closeHovers')" class="ok">{{ $t('buttons.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

@@ -21,7 +21,11 @@
</section>
<div>
<button type="submit" @click="$store.commit('closeHovers')" class="ok">{{ $t('buttons.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

@@ -6,7 +6,10 @@
<file-list @update:selected="val => dest = val"></file-list>
<div>
<button class="ok" @click="move">{{ $t('buttons.move') }}</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')"
@@ -18,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 {
@@ -48,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

@@ -4,7 +4,10 @@
<p>{{ $t('prompts.newDirMessage') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button class="ok" @click="submit">{{ $t('buttons.create') }}</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')"
@@ -15,7 +18,7 @@
<script>
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'new-dir',

View File

@@ -4,7 +4,10 @@
<p>{{ $t('prompts.newFileMessage') }}</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button class="ok" @click="submit">{{ $t('buttons.create') }}</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')"
@@ -15,7 +18,7 @@
<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,30 +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="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.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>
@@ -51,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 {
@@ -93,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

@@ -5,7 +5,10 @@
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
<div>
<button @click="submit" type="submit">{{ $t('buttons.rename') }}</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')"
@@ -17,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>{{ $t('buttons.ok') }}</button>
<button @click="close"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')"
autofocus>{{ $t('buttons.ok') }}</button>
</div>
</div>
</template>

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;
}

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

@@ -1,8 +1,10 @@
permanent: Permanent
buttons:
cancel: Cancel
close: Close
copy: Copy
copyFile: Copy file
copyToClipboard: Copy to clipboard
create: Create
delete: Delete
download: Download
@@ -13,17 +15,22 @@ buttons:
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.
@@ -42,6 +49,9 @@ files:
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:
@@ -81,16 +91,24 @@ prompts:
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.
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
@@ -114,17 +132,16 @@ settings:
You can set the user to be an administrator or choose the permissions
individually. If you select "Administrator", all of the other options will be
automatically checked. The management of users remains a privilege of an administrator.
pluginsUpdated: Plugins settings updated!
profileSettings: Profile Settings
ruleExample1: >
'prevents the access to any dot file (such as .git, .gitignore) in
every folder.'
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.'
Here you can define a set of allow and disallow rules for this specific
user. The blocked files won't show up in the listings and they wont be accessible
to the user. We support regex and paths relative to the users scope.
rulesHelp2: >
Each rule goes in one different line and must start with the keyword
{0} or {1}. Then you should write {2} if you are using a regular expression and
@@ -133,9 +150,9 @@ settings:
settingsUpdated: Settings updated!
user: User
userCommands: Commands
userCommandsHelp:
'A space separated list with the available commands for this user.
Example:'
userCommandsHelp: >
A space separated list with the available commands for this user.
Example:
userCreated: User created!
userDeleted: User deleted!
userManagement: User Management
@@ -150,15 +167,31 @@ sidebar:
newFolder: New folder
servedWith: Served with
settings: Settings
siteSettings: Site Settings
hugoNew: Hugo New
preview: Preview
search:
writeToSearch: Write here to 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:'
search: Search...
type: Type and press enter to search.
pressToSearch: Press enter to search.
pressToExecute: Press enter to execute.
types: Types
video: Video
writeToSearch: Write here to search
languages:
en: English
pt: Portuguese
zhCN: Chinese (Simplified)
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time:
unit: Time Unit
seconds: Seconds
minutes: Minutes
hours: Hours
days: Days

View File

@@ -2,7 +2,9 @@ import Vue from 'vue'
import VueI18n from 'vue-i18n'
import en from './en.yaml'
import pt from './pt.yaml'
import ja from './ja.yaml'
import zhCN from './zh-cn.yaml'
import zhTW from './zh-tw.yaml'
Vue.use(VueI18n)
@@ -12,7 +14,9 @@ const i18n = new VueI18n({
messages: {
'en': en,
'pt': pt,
'zh-cn': zhCN
'ja': ja,
'zh-cn': zhCN,
'zh-tw': zhTW
}
})

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

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

View File

@@ -1,8 +1,10 @@
permanent: Permanente
buttons:
cancel: Cancelar
close: Fechar
copy: Copiar
copyFile: Copiar ficheiro
copyToClipboard: Copiar
create: Criar
delete: Eliminar
download: Descarregar
@@ -14,9 +16,13 @@ buttons:
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
@@ -24,16 +30,17 @@ buttons:
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:
folders: Pastas
files: Ficheiros
body: Corpo
clear: Limpar
closePreview: Fechar pré-visualização
files: Ficheiros
folders: Pastas
home: Início
lastModified: Última modificação
loading: A carregar...
@@ -42,6 +49,9 @@ files:
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:
@@ -54,6 +64,12 @@ help:
f1: esta informação
f2: renomear ficheiro
help: Ajuda
languages:
en: English
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
login:
password: Palavra-passe
submit: Login
@@ -75,6 +91,8 @@ prompts:
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
@@ -83,20 +101,40 @@ prompts:
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,
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!
@@ -111,32 +149,32 @@ settings:
passwordUpdated: Palavra-passe atualizada!
permissions: Permissões
permissionsHelp: >
Pode definir o utilizador como administrador ou escolher as permissões manualmente.
Se selecionar a opção "Administrador", todas as outras opções serão automaticamente
selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores.
pluginsUpdated: Plugins atualizados!
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
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
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},
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:'
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
@@ -145,21 +183,18 @@ settings:
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
search:
writeToSearch: Escreva aqui para pesquisar
searchOrCommand: Pesquise ou execute um comando...
searchOrSupportedCommand: 'Pesquise ou utilize um dos seus comandos:'
search: Pesquise...
type: Escreva e prima enter para pesquisar.
pressToSearch: Prima enter para pesquisar.
pressToExecute: Prima enter para executar.
languages:
en: Inglês
pt: Português
zhCN: Chinês (Simplificado)
siteSettings: Configurações do Site
time:
unit: Unidades de Tempo
seconds: Segundos
minutes: Minutos
hours: Horas
days: Dias

View File

@@ -1,8 +1,10 @@
permanent: 永久
buttons:
cancel: 取消
close: 关闭
copy: 复制
copyFile: 复制文件
copyToClipboard: 复制到剪贴板
create: 创建
delete: 删除
download: 下载
@@ -11,94 +13,110 @@ buttons:
move: 移动
moveFile: 移动文件
new:
next: 下一
next: 下一
ok: 确定
previous: 以前
replace: 替换
previous: 上一个
rename: 重命名
reportIssue: 报告问题
save: 保存
search: 搜索
select: 选择
share: 分享
publish: 发布
selectMultiple: 选择多个
schedule: 计划
switchView: 切换显示方式
toggleSidebar: 切换侧边栏
update: 更新
upload: 上传
permalink: 获取永久链接
errors:
forbidden: 你被禁止访问.
internal: 内部出现麻烦了.
notFound: 找不到文件.
forbidden: 你被禁止访问
internal: 内部出现麻烦了
notFound: 找不到文件
files:
folders: 文件夹
files: 文件
body: Body
clear:
clear:
closePreview: 关闭预览
home: 主页
lastModified: 最后修改
loading: 加载中...
lonely: 这里没有任何文件...
metadata: 元数据
multipleSelectionEnabled: 启用多选模式(现在可以选择多个文件/文件夹)
multipleSelectionEnabled: 多选模式已开启
name: 名称
size: 大小
sortByName: 按名称排序
sortBySize: 按大小排序
sortByLastModified: 按最后修改时间排序
help:
click: 选择文件或目录
ctrl:
click: 选择多个文件或目录
f: 打开搜索框
s: 保存文件或下载文件夹
del: 删除 所选文件/文件夹
doubleClick: 打开文件或目录
esc: 清除 当前所有选择 或 关闭提示信息
f1: 显示 当前帮助信息
f2: 重命名 文件/文件夹
s: 保存文件或下载当前文件夹
del: 删除所选文件/文件夹
doubleClick: 打开文件/文件夹
esc: 清除已选项或关闭提示信息
f1: 显示帮助信息
f2: 重命名文件/文件夹
help: 帮助
login:
password: 密码
submit: 登录
username: 用户名
wrongCredentials: 账号或密码错误
wrongCredentials: 用户名或密码错误
prompts:
copy: 复制
copyMessage: '请选择欲复制至的目录:'
currentlyNavigating: '目前正在浏览:'
copyMessage: 请选择欲复制至的目录
currentlyNavigating: 当前目录:
deleteMessageMultiple: 你确定要删除这 {count} 个文件吗?
deleteMessageSingle: 你确定要删除这个文件/文件夹吗?
deleteTitle: 删除文件
displayName: '名称:'
displayName: 名称:
download: 下载文件
downloadMessage: 请选择要下载的压缩格式.
downloadMessage: 请选择要下载的压缩格式
error: 出了一点问题...
fileInfo: 文件信息
filesSelected: '选择 {count} 个文件.'
filesSelected: 选择 {count} 个文件
lastModified: 最后修改
move: 移动
moveMessage: '请选择欲移动至的目录:'
moveMessage: 请选择欲移动至的目录
newDir: 新建目录
newDirMessage: 请输入新目录的名称.
newDirMessage: 请输入新目录的名称
newFile: 新建文件
newFileMessage: 请输入新文件的名称.
newFileMessage: 请输入新文件的名称
numberDirs: 目录数
numberFiles: 文件数
replace: 替换
replaceMessage: "\
您尝试上传的文件中有一个与现有文件的名称存在冲突。\
是否替换现有的同名文件?"
rename: 重命名
renameMessage: '请输入新名称, 旧名称是:'
renameMessage: 请输入新名称旧名称为:
show: 揭示
size: 大小
schedule: 计划
scheduleMessage: 请选择发布这篇帖子的日期。
newArchetype: 创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。
settings:
admin: 管理员
administrator: 管理员
allowCommands: 执行命令(Linux 代码)
allowEdit: 编辑、重命名或删除文件/目录.
allowNew: 创建新文件和目录.
allowEdit: 编辑、重命名或删除文件/目录
allowNew: 创建新文件和目录
allowPublish: 发布新的帖子与页面
avoidChanges: '(留空以避免更改)'
changePassword: 更改密码
commands: 命令(linux 代码)
commandsHelp: >
'Here you can set commands that are executed in the named events.
每行一条命令. If the event is related to files, such as before and after saving,
the environment variable "file" will be available with the path of the file.'
commandsUpdated: 命令更新!
commandsHelp: "\
在这里,您可以设置在指定事件下执行的命令,一行一条。\
若事件与文件相关,如“在保存文件前”,\
则文件的路径会被赋值给环境变量 \"file\"。"
commandsUpdated: 命令更新
customStylesheet: 自定义样式表
examples: 例子
globalSettings: 全局设置
@@ -107,51 +125,71 @@ settings:
newPasswordConfirm: 重输一遍新密码
newUser: 新建用户
password: 密码
passwordUpdated: 密码更新!
passwordUpdated: 密码更新
permissions: 权限
permissionsHelp: >
'您可以将该用户设置为管理员单独选择各项权限. 如果选择 "管理员(Administrator)" ,
将自动检查所有其他选项, 并且该用户可以管理其他用户.'
pluginsUpdated: 插件设置更新!
permissionsHelp: "\
您可以将该用户设置为管理员,也可以单独选择各项权限。\
如果选择了“管理员”,则其他的选项会被自动勾上,\
同时该用户可以管理其他用户。"
profileSettings: 配置文件设置
ruleExample1: >
'阻止用户访问每个文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore).'
ruleExample2: 阻止用户访问其目录范围内任何名为 Caddyfile 的文件/文件夹.
ruleExample1: "\
阻止用户访问所有文件夹下任何以 . 开头的文件\
(隐藏文件, 例如: .git, .gitignore)。"
ruleExample2: 阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。
rules: 规则
rulesHelp1: >
'这里您可以为特定用户制定一组允许或不允许的规则,
阻止的文件将不会显示列表中, 用户将无法访问, 支持相对于用户的范围.'
rulesHelp2: >
每行一条规则, 必须以关键词 {0} 或 {1} 开头. 如果使用正则表达式,
然后使用表达式或路径, 则需要在第二列单词加入 {2} .
rulesHelp1: "\
您可以为用户制定一组黑名单或白名单式的规则\
被屏蔽的文件将不会显示列表中,用户也无权限访问,\
支持相对于目录范围的路径。"
rulesHelp2: "\
每行一条规则,且必须以关键词 {0} 或 {1} 开头。\
如要使用正则表达式,请在加上 {2} 之后再附上表达式或路径。"
scope: 目录范围
settingsUpdated: 设置更新!
settingsUpdated: 设置更新
user: 用户
userCommands: 用户命令(Linux 代码)
userCommandsHelp: '一个以空格分割的列表, 用于指定该用户可以执行的命令(Linux 代码), 例如:'
userCreated: 用户创建!
userDeleted: 用户删除!
userCommandsHelp: "\
指定该用户可以执行的命令(Linux 代码),用空格分隔。\
例如:"
userCreated: 用户已创建!
userDeleted: 用户已删除!
userManagement: 用户管理
username: 用户名
users: 用户
userUpdated: 用户更新!
userUpdated: 用户更新
sidebar:
help: 帮助
logout: 注销
logout: 登出
myFiles: 我的文件
newFile: 新建文件
newFolder: 新建文件夹
servedWith: 服务提供
servedWith: '服务提供者:'
settings: 设置
siteSettings: 网站设置
hugoNew: Hugo New
preview: 预览
search:
writeToSearch: 请输入要搜索的内容
searchOrCommand: 搜索或者执行命令(Linux 代码)...
searchOrSupportedCommand: '搜索或使用您支持使用的命令(一次只能执行一个命令):'
images: 图像
music: 音乐
pdf: PDF
pressToExecute: 按回车键执行。
pressToSearch: 按回车键搜索。
search: 搜索...
type: 键入并按 Enter 键(回车)进行搜索.
pressToSearch: 按 Enter 键(回车)进行搜索.
pressToExecute: 按 Enter 键(回车)执行.
searchOrCommand: 搜索或者执行命令(Linux 代码)...
searchOrSupportedCommand: 搜索或使用您可以使用的命令(一次只能执行一个命令)
type: 键入并按回车键进行搜索。
types: 类型
video: 视频
writeToSearch: 请输入要搜索的内容
languages:
en: English
pt: Portuguese
zhCN: Chinese (Simplified)
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time:
unit: 时间单位
seconds:
minutes: 分钟
hours: 小时
days:

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

@@ -0,0 +1,194 @@
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: 執行命令
allowEdit: 編輯、重命名或刪除檔案/目錄
allowNew: 創建新檔案和目錄
allowPublish: 發佈新的帖子與頁面
avoidChanges: '(留空以避免更改)'
changePassword: 更改密碼
commands: 命令
commandsHelp: "\
在這裡,您可以設定在指定事件下執行的命令,一行一條。\
若事件與檔案相關,如“在保存檔案前”,\
則檔案的路徑會被賦值給環境變數 \"file\"。"
commandsUpdated: 命令已更新!
customStylesheet: 自定義樣式表
examples: 例子
globalSettings: 全域設定
language: 語言
newPassword: 您的新密碼
newPasswordConfirm: 重輸一遍新密碼
newUser: 建立用戶
password: 密碼
passwordUpdated: 密碼已更新!
permissions: 權限
permissionsHelp: "\
您可以將該用戶設置為管理員,也可以單獨選擇各項權限。\
如果選擇了“管理員”,則其他的選項會被自動勾上,\
同時該用戶可以管理其他用戶。"
profileSettings: 設定檔設定
ruleExample1: "\
封鎖用戶訪問所有資料夾下任何以 . 開頭的檔案\
(隱藏文件, 例如: .git, .gitignore)。"
ruleExample2: 封鎖用戶訪問其目錄範圍的根目錄下名為 Caddyfile 的檔案。
rules: 規則
rulesHelp1: "\
您可以為該用戶製定一組黑名單或白名單式的規則,\
被屏蔽的檔案將不會顯示在清單中,用戶也無權限訪問,\
支持相對於目錄範圍的路徑。"
rulesHelp2: "\
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
scope: 目錄範圍
settingsUpdated: 設定已更新!
user: 用戶
userCommands: 用戶命令
userCommandsHelp: "\
指定該用戶可以執行的命令,用空格分隔。\
例如:"
userCreated: 用戶已建立!
userDeleted: 用戶已刪除!
userManagement: 用戶管理
username: 用戶名
users: 用戶
userUpdated: 用戶已更新!
sidebar:
help: 幫助
logout: 登出
myFiles: 我的檔案
newFile: 建立檔案
newFolder: 建立資料夾
servedWith: '服務提供者:'
settings: 設定
siteSettings: 網站設定
hugoNew: Hugo New
preview: 預覽
search:
images: 影像
music: 音樂
pdf: PDF
pressToExecute: 按確定鍵執行。
pressToSearch: 按確定鍵搜尋。
search: 搜尋...
searchOrCommand: 搜尋或者執行命令...
searchOrSupportedCommand: 搜尋或使用您可以使用的命令(一次只能執行一個命令)
type: 輸入並按確定鍵進行搜尋。
types: 類型
video: 影片
writeToSearch: 請輸入要搜尋的內容
languages:
en: English
pt: Português
ja: 日本語
zhCN: 中文 (简体)
time:
unit: 時間單位
seconds:
minutes: 分鐘
hours: 小時
days:

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,4 +1,5 @@
import i18n from '@/i18n'
import moment from 'moment'
const mutations = {
closeHovers: state => {
@@ -13,6 +14,7 @@ const mutations = {
state.show = value.prompt
state.showMessage = value.message
state.showConfirm = value.confirm
},
showError: (state, value) => {
state.show = 'error'
@@ -25,12 +27,16 @@ const mutations = {
setLoading: (state, value) => { state.loading = value },
setReload: (state, value) => { state.reload = value },
setUser: (state, value) => {
moment.locale(value.locale)
i18n.locale = value.locale
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
@@ -52,6 +58,9 @@ const mutations = {
resetClipboard: (state) => {
state.clipboard.key = ''
state.clipboard.items = []
},
setSchedule: (state, value) => {
state.schedule = value
}
}

View File

@@ -4,9 +4,11 @@ const ssl = (window.location.protocol === 'https:')
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
}
@@ -33,7 +35,7 @@ export function fetch (url) {
})
}
export function rm (url) {
export function remove (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -54,7 +56,7 @@ export function rm (url) {
})
}
export function post (url, content = '') {
export function post (url, content = '', overwrite = false) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@@ -62,26 +64,39 @@ export 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)
})
}
export 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) {
@@ -368,25 +383,69 @@ export function deleteUser (id) {
})
}
export default {
removePrefix,
delete: rm,
fetch,
checksum,
move,
put,
copy,
post,
command,
search,
download,
// other things
getSettings,
updateSettings,
// User things
newUser,
getUser,
getUsers,
updateUser,
deleteUser
// SHARE
export function getShare (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve(JSON.parse(request.responseText))
} else {
reject(request.status)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
export function deleteShare (hash) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve()
} else {
reject(request.status)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
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('POST', url, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve(JSON.parse(request.responseText))
} else {
reject(request.responseStatus)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}

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

@@ -33,7 +33,7 @@ import InternalError from './errors/500'
import Preview from '@/components/files/Preview'
import Listing from '@/components/files/Listing'
import Editor from '@/components/files/Editor'
import api from '@/utils/api'
import * as api from '@/utils/api'
import { mapGetters, mapState, mapMutations } from 'vuex'
export default {

View File

@@ -15,17 +15,15 @@
<h1>{{ $t('settings.globalSettings') }}</h1>
<form @submit="savePlugin" v-if="plugins.length > 0">
<template v-for="plugin in plugins">
<h2>{{ capitalize(plugin.name) }}</h2>
<form @submit="saveStaticGen" v-if="$store.state.staticGen.length > 0">
<h2>{{ capitalize($store.state.staticGen) }}</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 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>
@@ -55,7 +53,7 @@ export default {
data: function () {
return {
commands: [],
plugins: []
staticGen: []
}
},
computed: {
@@ -64,8 +62,8 @@ export default {
created () {
getSettings()
.then(settings => {
for (let key in settings.plugins) {
this.plugins.push(this.parsePlugin(key, settings.plugins[key]))
if (this.$store.state.staticGen.length > 0) {
this.parseStaticGen(settings.staticGen)
}
for (let key in settings.commands) {
@@ -108,40 +106,29 @@ export default {
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
.catch(error => { this.showError(error) })
},
savePlugin (event) {
saveStaticGen (event) {
event.preventDefault()
let plugins = {}
let staticGen = {}
for (let plugin of this.plugins) {
let p = {}
for (let field of this.staticGen) {
staticGen[field.variable] = field.value
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
if (field.original === 'array') {
let val = field.value.split(' ')
if (val[0] === '') {
val.shift()
}
}
plugins[plugin.name] = p
staticGen[field.variable] = val
}
}
updateSettings(plugins, 'plugins')
.then(() => { this.showSuccess(this.$t('settings.pluginsUpdated')) })
updateSettings(staticGen, 'staticGen')
.then(() => { this.showSuccess(this.$t('settings.settingsUpdated')) })
.catch(error => { this.showError(error) })
},
parsePlugin (name, plugin) {
let obj = {
name: name,
fields: []
}
for (let option of plugin) {
parseStaticGen (staticgen) {
for (let option of staticgen) {
let value = option.value
let field = {
@@ -156,7 +143,7 @@ export default {
field.original = 'array'
field.value = value.join(' ')
obj.fields.push(field)
this.staticGen.push(field)
continue
}
@@ -167,10 +154,8 @@ export default {
break
}
obj.fields.push(field)
this.staticGen.push(field)
}
return obj
}
}
}

View File

@@ -1,6 +1,15 @@
<template>
<div>
<form @submit="save" class="dashboard">
<ul id="nav">
<li>
<router-link to="/users">
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.userManagement') }}
</router-link>
</li>
<li></li>
</ul>
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
@@ -19,9 +28,7 @@
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
<p v-for="(value, key) in permissions" :key="key">
<input type="checkbox" :disabled="admin" v-model="permissions[key]"> {{ capitalize(key) }}
</p>
<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>
@@ -85,6 +92,7 @@ export default {
allowNew: false,
allowEdit: false,
allowCommands: false,
allowPublish: false,
permissions: {},
password: '',
username: '',
@@ -111,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
}
@@ -131,6 +140,7 @@ export default {
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(' ')
@@ -174,6 +184,7 @@ export default {
this.admin = false
this.allowNew = false
this.allowEdit = false
this.allowPublish = false
this.permissins = {}
this.allowCommands = false
this.password = ''
@@ -232,6 +243,7 @@ export default {
allowCommands: this.allowCommands,
allowNew: this.allowNew,
allowEdit: this.allowEdit,
allowPublish: this.allowPublish,
permissions: this.permissions,
css: this.css,
locale: this.locale,

View File

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

View File

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

View File

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

View File

@@ -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,13 +25,14 @@ 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"
)
@@ -48,10 +47,11 @@ 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.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
flag.StringVar(&locale, "locale", "en", "Default locale for new users")
flag.StringVar(&plugin, "plugin", "", "Plugin you want to enable")
flag.StringVar(&staticgen, "staticgen", "", "Static Generator you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
}
@@ -65,7 +65,8 @@ 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)
@@ -78,21 +79,34 @@ 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("AllowPublish", flag.Lookup("allow-publish"))
viper.BindPFlag("Locale", flag.Lookup("locale"))
viper.BindPFlag("Plugin", flag.Lookup("plugin"))
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 {
fmt.Println("filemanager version", version)
os.Exit(0)
printVersion()
}
// Add a configuration file if set.
@@ -139,6 +153,7 @@ 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"),
@@ -154,21 +169,27 @@ func main() {
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

@@ -53,7 +53,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// If the format is true, just set it to "zip".
if query == "true" {
if query == "true" || query == "" {
query = "zip"
}

View File

@@ -20,7 +20,7 @@ import (
"strings"
"time"
"github.com/spf13/hugo/parser"
"github.com/gohugoio/hugo/parser"
)
var (

View File

@@ -62,11 +62,13 @@ 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 (
@@ -78,7 +80,6 @@ var (
errEmptyScope = errors.New("scope is empty")
errWrongDataType = errors.New("wrong data type")
errInvalidUpdateField = errors.New("invalid field to update")
plugins = map[string]Plugin{}
)
// FileManager is a file manager instance. It should be creating using the
@@ -93,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.
@@ -107,6 +111,11 @@ type FileManager struct {
// there will only exist one user, called "admin".
NoAuth bool
// staticgen is the name of the current static website generator.
staticgen string
// StaticGen is the static websit generator handler.
StaticGen StaticGen
// The Default User needed to build the New User page.
DefaultUser *User
@@ -115,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.
@@ -151,10 +157,10 @@ type User struct {
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"`
@@ -181,40 +187,12 @@ 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: "",
@@ -231,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
@@ -267,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)
}
@@ -306,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 {
@@ -322,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
}
@@ -349,95 +334,7 @@ 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{
FileManager: m,
@@ -461,6 +358,87 @@ func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
// 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)
}
}
}
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule

119
http.go
View File

@@ -6,6 +6,9 @@ import (
"net/http"
"os"
"strings"
"time"
"github.com/asdine/storm"
)
// RequestContext contains the needed information to make handlers work.
@@ -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, w,
c.assets.MustString("sw.js"),
"application/javascript",
c,
)
}
@@ -58,16 +60,27 @@ 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, w,
c.assets.MustString("index.html"),
"text/html",
c,
)
}
@@ -79,10 +92,9 @@ func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
}
return renderFile(
w,
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.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
}
@@ -140,21 +159,12 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
code, err = usersHandler(c, w, r)
case "settings":
code, err = settingsHandler(c, w, r)
case "share":
code, err = shareHandler(c, w, r)
default:
code = http.StatusNotFound
}
if code >= 300 || err != nil {
return code, err
}
for p := range c.Plugins {
code, err := plugins[p].Handler.After(c, w, r)
if code != 0 || err != nil {
return code, err
}
}
return code, err
}
@@ -191,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.Plugins {
javascript += plugins[name].JavaScript + "\n"
}
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(),
"JavaScript": template.JS(javascript),
"BaseURL": c.RootURL(),
"StaticGen": c.staticgen,
})
if err != nil {
return http.StatusInternalServerError, err
@@ -211,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,6 +9,7 @@
"lint": "eslint --ext .js,.vue assets/src"
},
"dependencies": {
"clipboard": "^1.7.1",
"codemirror": "^5.27.4",
"filesize": "^3.5.10",
"moment": "^2.18.1",

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.Plugins["hugo"].(*Hugo)
// If we are using the 'magic url' for the settings, we should redirect the
// request for the acutual path.
if r.URL.Path == "/settings/" || r.URL.Path == "/settings" {
var frontmatter string
var err error
if _, err = os.Stat(filepath.Join(o.Root, "config.yaml")); err == nil {
frontmatter = "yaml"
}
if _, err = os.Stat(filepath.Join(o.Root, "config.json")); err == nil {
frontmatter = "json"
}
if _, err = os.Stat(filepath.Join(o.Root, "config.toml")); err == nil {
frontmatter = "toml"
}
r.URL.Path = "/config." + frontmatter
}
// From here on, we only care about 'hugo' router so we can bypass
// the others.
if c.Router != "hugo" {
return 0, nil
}
// If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method.
if r.Method != http.MethodPost {
return http.StatusMethodNotAllowed, nil
}
// If we are creating a file built from an archetype.
if r.Header.Get("Archetype") != "" {
if !c.User.AllowNew {
return http.StatusForbidden, nil
}
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
archetype := r.Header.Get("archetype")
ext := filepath.Ext(filename)
// If the request isn't for a markdown file, we can't
// handle it.
if ext != ".markdown" && ext != ".md" {
return http.StatusBadRequest, ErrUnsupportedFileType
}
// Tries to create a new file based on this archetype.
args := []string{"new", filename, "--kind", archetype}
if err := Run(o.Exe, args, o.Root); err != nil {
return http.StatusInternalServerError, err
}
// Writes the location of the new file to the Header.
w.Header().Set("Location", "/files/content/"+filename)
return http.StatusCreated, nil
}
// If we are trying to regenerate the website.
if r.Header.Get("Regenerate") == "true" {
if !c.User.Permissions["allowPublish"] {
return http.StatusForbidden, nil
}
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// Before save command handler.
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
// We only run undraft command if it is a file.
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
if err := o.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
}
// Regenerates the file
o.run(false)
// Executed the before publish command.
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}
if r.Header.Get("Schedule") != "" {
if !c.User.Permissions["allowPublish"] {
return http.StatusForbidden, nil
}
return o.schedule(c, w, r)
}
return http.StatusNotFound, nil
}
func (h hugo) After(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}

View File

@@ -1,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"
)
@@ -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 @@
7327806da8feadd5f82e2286efb2e2dd44109d3e
4b205fed13570c943ad67f9fc4db4f51aeb62cec

View File

@@ -1,6 +1,7 @@
package filemanager
import (
"bytes"
"encoding/json"
"net/http"
"reflect"
@@ -11,12 +12,12 @@ import (
type modifySettingsRequest struct {
*modifyRequest
Data struct {
Commands map[string][]string `json:"commands"`
Plugins map[string]map[string]interface{} `json:"plugins"`
Commands map[string][]string `json:"commands"`
StaticGen map[string]interface{} `json:"staticGen"`
} `json:"data"`
}
type pluginOption struct {
type option struct {
Variable string `json:"variable"`
Name string `json:"name"`
Value interface{} `json:"value"`
@@ -59,8 +60,8 @@ func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
type settingsGetRequest struct {
Commands map[string][]string `json:"commands"`
Plugins map[string][]pluginOption `json:"plugins"`
Commands map[string][]string `json:"commands"`
StaticGen []option `json:"staticGen"`
}
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
@@ -69,19 +70,22 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
}
result := &settingsGetRequest{
Commands: c.Commands,
Plugins: map[string][]pluginOption{},
Commands: c.Commands,
StaticGen: []option{},
}
for name, p := range c.Plugins {
result.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++ {
result.Plugins[name] = append(result.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(),
})
}
}
@@ -108,18 +112,16 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusOK, nil
}
// Update the plugins.
if mod.Which == "plugins" {
for name, plugin := range mod.Data.Plugins {
err = mapstructure.Decode(plugin, c.Plugins[name])
if err != nil {
return http.StatusInternalServerError, err
}
// 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("plugins", name, c.Plugins[name])
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

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
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"net/http"
"os"
"sort"
"strconv"
"strings"
@@ -175,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 {
@@ -202,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
@@ -308,6 +337,11 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
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.
if u.Rules == nil {
u.Rules = []*Rule{}
@@ -344,11 +378,6 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.Password = suser.Password
}
// Default permissions if current are nil.
if u.Permissions == nil {
u.Permissions = c.DefaultUser.Permissions
}
// Updates the whole User struct because we always are supposed
// to send a new entire object.
err = c.db.Save(u)

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 {