Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a67a9779e9 | ||
|
|
059c0dc071 | ||
|
|
89a8360c4c | ||
|
|
4c8b118848 | ||
|
|
146744ea93 | ||
|
|
689077c545 | ||
|
|
53bba54ce4 | ||
|
|
d8974ce3af | ||
|
|
8d715bb433 | ||
|
|
25a86a9382 | ||
|
|
97f31310c6 | ||
|
|
97b5cea521 | ||
|
|
2462346e56 | ||
|
|
aaf6d60c3c | ||
|
|
3ff8908047 | ||
|
|
d5e943069e | ||
|
|
30be924648 | ||
|
|
bfdb924cb7 | ||
|
|
6d853d63ed | ||
|
|
4bae90c80f | ||
|
|
e464eb69ed | ||
|
|
fee9330cfa | ||
|
|
b034c87ec0 | ||
|
|
87420bcefd | ||
|
|
d6ca579519 | ||
|
|
d74c9ae5af | ||
|
|
2f60562143 |
@@ -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;
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -17,7 +17,49 @@
|
||||
|
||||
<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.path">
|
||||
@@ -28,11 +70,11 @@
|
||||
</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>
|
||||
@@ -40,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',
|
||||
@@ -66,6 +108,8 @@ export default {
|
||||
this.$store.commit('setReload', true)
|
||||
}
|
||||
|
||||
document.body.style.overflow = 'auto'
|
||||
this.reset()
|
||||
this.$refs.input.blur()
|
||||
}
|
||||
|
||||
@@ -74,6 +118,7 @@ export default {
|
||||
if (val === 'search') {
|
||||
this.reload = false
|
||||
this.$refs.input.focus()
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -123,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++) {
|
||||
@@ -146,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
|
||||
@@ -160,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
21
assets/src/components/buttons/Schedule.vue
Normal file
21
assets/src/components/buttons/Schedule.vue
Normal 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>
|
||||
17
assets/src/components/buttons/Share.vue
Normal file
17
assets/src/components/buttons/Share.vue
Normal 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>
|
||||
@@ -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', '')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="req.display"
|
||||
@drop="drop"
|
||||
@dragenter="dragEnter"
|
||||
@dragend="dragEnd">
|
||||
<div>
|
||||
@@ -92,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 {
|
||||
@@ -230,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')
|
||||
@@ -239,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 => {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@click="click"
|
||||
@dblclick="open"
|
||||
@touchstart="touchstart"
|
||||
:data-dir="isDir"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected">
|
||||
<div>
|
||||
@@ -32,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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -21,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 {
|
||||
@@ -51,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 => {
|
||||
|
||||
@@ -17,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'
|
||||
|
||||
@@ -36,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 => {
|
||||
@@ -59,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 => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -19,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',
|
||||
|
||||
@@ -34,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',
|
||||
|
||||
@@ -21,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 {
|
||||
@@ -51,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 => {
|
||||
|
||||
68
assets/src/components/prompts/NewArchetype.vue
Normal file
68
assets/src/components/prompts/NewArchetype.vue
Normal 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>
|
||||
|
||||
@@ -18,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',
|
||||
|
||||
@@ -18,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',
|
||||
|
||||
@@ -11,33 +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"
|
||||
:aria-label="prompt.ok"
|
||||
:title="prompt.ok"
|
||||
:value="prompt.ok">
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
@@ -54,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 {
|
||||
@@ -96,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')
|
||||
}
|
||||
|
||||
@@ -20,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',
|
||||
|
||||
26
assets/src/components/prompts/Replace.vue
Normal file
26
assets/src/components/prompts/Replace.vue
Normal 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>
|
||||
41
assets/src/components/prompts/Schedule.vue
Normal file
41
assets/src/components/prompts/Schedule.vue
Normal 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>
|
||||
|
||||
153
assets/src/components/prompts/Share.vue
Normal file
153
assets/src/components/prompts/Share.vue
Normal 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>
|
||||
|
||||
@@ -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,7 +172,7 @@ header .search-button {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#search.ongoing #result p {
|
||||
#search.ongoing #result #renew {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -179,7 +180,7 @@ header .search-button {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#search.active #result > p > i {
|
||||
#search.active #result>p>i {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
display: table;
|
||||
@@ -212,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;
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -84,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
|
||||
@@ -117,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
|
||||
@@ -136,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
|
||||
@@ -153,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
|
||||
|
||||
@@ -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
197
assets/src/i18n/ja.yaml
Normal 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: 日
|
||||
@@ -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,9 +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
|
||||
sortByLastModified: Ordenar pela última modificação
|
||||
help:
|
||||
click: selecionar pasta ou ficheiro
|
||||
ctrl:
|
||||
@@ -57,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
|
||||
@@ -78,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
|
||||
@@ -86,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!
|
||||
@@ -114,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
|
||||
@@ -148,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
|
||||
|
||||
@@ -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
194
assets/src/i18n/zh-tw.yaml
Normal 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: 天
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
50
assets/static/share/404.html
Normal file
50
assets/static/share/404.html
Normal 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>
|
||||
85
assets/static/share/index.html
Normal file
85
assets/static/share/index.html
Normal 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>
|
||||
@@ -139,6 +139,7 @@ func parse(c *caddy.Controller) ([]*config, error) {
|
||||
}
|
||||
|
||||
fm, err := New(database, User{
|
||||
Locale: "en",
|
||||
AllowCommands: true,
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/filemanager/plugins"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
@@ -108,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,
|
||||
@@ -127,20 +127,15 @@ 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
|
||||
}
|
||||
|
||||
177
caddy/jekyll/jekyll.go
Normal file
177
caddy/jekyll/jekyll.go
Normal 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
|
||||
}
|
||||
@@ -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,8 +79,9 @@ 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")
|
||||
@@ -151,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"),
|
||||
@@ -166,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
2
file.go
2
file.go
@@ -20,7 +20,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
240
filemanager.go
240
filemanager.go
@@ -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
119
http.go
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
228
plugins/hugo.go
228
plugins/hugo.go
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
})()`
|
||||
@@ -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
|
||||
}
|
||||
76
resource.go
76
resource.go
@@ -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 {
|
||||
|
||||
@@ -1 +1 @@
|
||||
2678061f4a5fd54d606c3bbaa3aa899a5e55cf37
|
||||
4b205fed13570c943ad67f9fc4db4f51aeb62cec
|
||||
48
settings.go
48
settings.go
@@ -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
137
share.go
Normal 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
307
staticgen.go
Normal 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
|
||||
}
|
||||
39
users.go
39
users.go
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user