Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cac5413424 | ||
|
|
037efc2eb8 | ||
|
|
7de22b53b8 | ||
|
|
ae19731015 | ||
|
|
8aa0797019 | ||
|
|
17b3a403a5 | ||
|
|
819d511690 | ||
|
|
30cfd06e3d | ||
|
|
c5558e6e41 | ||
|
|
c3b3099ebb | ||
|
|
b1d47daa69 | ||
|
|
f51e2d5ba1 | ||
|
|
386974657e | ||
|
|
f7858cd719 | ||
|
|
b355a5c058 | ||
|
|
50758b53f4 | ||
|
|
f9902d2bdb | ||
|
|
b1512d2b66 | ||
|
|
1cfd31756d | ||
|
|
9152c77543 | ||
|
|
c236db329f | ||
|
|
bf6e0abd96 | ||
|
|
8d8c756233 | ||
|
|
d5f0471ab7 | ||
|
|
f7aaae3f63 | ||
|
|
2515819026 | ||
|
|
7ad727d27d | ||
|
|
adbff03274 | ||
|
|
6b8a65382f | ||
|
|
4f7e6cbb52 | ||
|
|
40fe081962 | ||
|
|
ebf1325126 | ||
|
|
ea1b9febb7 | ||
|
|
f3edf63fb2 | ||
|
|
f13e7ba940 | ||
|
|
3ebe219e96 | ||
|
|
d838856711 | ||
|
|
610d55c26f | ||
|
|
ac044016ee | ||
|
|
58af3461a8 | ||
|
|
4191a6f9e0 | ||
|
|
c18ca4702d | ||
|
|
73b1094602 | ||
|
|
5d026ac15d | ||
|
|
21156bf24f | ||
|
|
edaf6d27aa | ||
|
|
816cfb2a6e | ||
|
|
269ec9ea4b | ||
|
|
9186c1f36c | ||
|
|
d5cefa20b3 | ||
|
|
1be7d7d256 | ||
|
|
1b0f67c0f6 | ||
|
|
67dbf88eb6 | ||
|
|
98bea91edf | ||
|
|
7747fa8ec3 | ||
|
|
d0cf6c08e8 | ||
|
|
600723c224 | ||
|
|
44ab20964c | ||
|
|
764289e52f | ||
|
|
741e5c84ea | ||
|
|
4b602be5e3 | ||
|
|
e4144ad2b2 | ||
|
|
4071a58107 | ||
|
|
04b6ca6015 | ||
|
|
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 | ||
|
|
1527ca0c50 | ||
|
|
0dee98b40a | ||
|
|
412ac9c9d6 | ||
|
|
9c2609995a | ||
|
|
68cb4ee980 | ||
|
|
adc82dd85e | ||
|
|
4f3375ee8d | ||
|
|
bda4fc16eb | ||
|
|
aa219e701e | ||
|
|
2460fd4fae | ||
|
|
d50bec8caa | ||
|
|
a5a68a8944 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"port": 80,
|
"port": 80,
|
||||||
"address": "",
|
"address": "",
|
||||||
"database": "/etc/database.db",
|
"database": "/database.db",
|
||||||
"scope": "/srv",
|
"scope": "/srv",
|
||||||
"allowCommands": true,
|
"allowCommands": true,
|
||||||
"allowEdit": true,
|
"allowEdit": true,
|
||||||
|
|||||||
13
Dockerfile
13
Dockerfile
@@ -7,15 +7,16 @@ RUN apk add --no-cache git
|
|||||||
RUN go get ./...
|
RUN go get ./...
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager
|
WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager
|
||||||
RUN go install
|
RUN CGO_ENABLED=0 go build -a
|
||||||
|
RUN mv filemanager /go/bin/filemanager
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM scratch
|
||||||
COPY --from=0 /go/bin/filemanager /usr/local/bin/filemanager
|
COPY --from=0 /go/bin/filemanager /filemanager
|
||||||
|
|
||||||
VOLUME /srv
|
VOLUME /srv
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
COPY Docker.json /etc/config.json
|
COPY Docker.json /config.json
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/filemanager"]
|
ENTRYPOINT ["/filemanager"]
|
||||||
CMD ["--config", "/etc/config.json"]
|
CMD ["--config", "/config.json"]
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(yml|yaml)$/,
|
||||||
|
loader: 'yml-loader'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(js|vue)$/,
|
test: /\.(js|vue)$/,
|
||||||
loader: 'eslint-loader',
|
loader: 'eslint-loader',
|
||||||
|
|||||||
@@ -5,10 +5,13 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
<meta name="base" content="{{ .BaseURL }}">
|
<meta name="base" content="{{ .BaseURL }}">
|
||||||
|
<meta name="staticgen" content="{{ .StaticGen }}">
|
||||||
|
<meta name="noauth" content="{{ .NoAuth }}">
|
||||||
|
<meta name="version" content="{{ .Version }}">
|
||||||
<title>File Manager</title>
|
<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="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">
|
<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 -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
|
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
|
||||||
<meta name="theme-color" content="#2979ff">
|
<meta name="theme-color" content="#2979ff">
|
||||||
@@ -22,13 +25,13 @@
|
|||||||
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
|
||||||
<meta name="msapplication-TileColor" content="#2979ff">
|
<meta name="msapplication-TileColor" content="#2979ff">
|
||||||
|
|
||||||
|
<script>CSS = "{{ .CSS }}"</script>
|
||||||
|
|
||||||
<% for (var chunk of webpack.chunks) {
|
<% for (var chunk of webpack.chunks) {
|
||||||
for (var file of chunk.files) {
|
for (var file of chunk.files) {
|
||||||
if (file.match(/\.(js|css)$/)) { %>
|
if (file.match(/\.(js|css)$/)) { %>
|
||||||
<link rel="<%= chunk.initial?'preload':'prefetch' %>" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
||||||
|
|
||||||
<!-- Plugins info -->
|
|
||||||
<script>{{ .JavaScript }}</script>
|
|
||||||
<style>
|
<style>
|
||||||
#loading {
|
#loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view></router-view>
|
<router-view @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
mounted: function () {
|
mounted () {
|
||||||
// Remove loading animation.
|
// Remove loading animation.
|
||||||
let loading = document.getElementById('loading')
|
let loading = document.getElementById('loading')
|
||||||
loading.classList.add('done')
|
loading.classList.add('done')
|
||||||
@@ -13,6 +13,36 @@ export default {
|
|||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
loading.parentNode.removeChild(loading)
|
loading.parentNode.removeChild(loading)
|
||||||
}, 200)
|
}, 200)
|
||||||
|
|
||||||
|
this.updateCSS()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateCSS (global = false) {
|
||||||
|
let css = this.$store.state.css
|
||||||
|
|
||||||
|
if (typeof this.$store.state.user.css === 'string' && !global) {
|
||||||
|
css += '\n' + this.$store.state.user.css
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeCSS()
|
||||||
|
|
||||||
|
let style = document.createElement('style')
|
||||||
|
style.title = 'custom-css'
|
||||||
|
style.type = 'text/css'
|
||||||
|
style.appendChild(document.createTextNode(css))
|
||||||
|
document.head.appendChild(style)
|
||||||
|
},
|
||||||
|
removeCSS () {
|
||||||
|
let style = document.querySelector('style[title="custom-css"]')
|
||||||
|
if (style === undefined || style === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
style.parentElement.removeChild(style)
|
||||||
|
},
|
||||||
|
cleanCSS () {
|
||||||
|
this.updateCSS(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="dashboard">
|
|
||||||
<h1>Global Settings</h1>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><router-link to="/settings/profile">Go to Profile Settings</router-link></li>
|
|
||||||
<li><router-link to="/users">Go to User Management</router-link></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<form @submit="savePlugin" v-if="plugins.length > 0">
|
|
||||||
<template v-for="plugin in plugins">
|
|
||||||
<h2>{{ capitalize(plugin.name) }}</h2>
|
|
||||||
|
|
||||||
<p v-for="field in plugin.fields" :key="field.variable">
|
|
||||||
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
|
|
||||||
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
|
|
||||||
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
|
|
||||||
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<p><input type="submit" value="Save"></p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form @submit="saveCommands">
|
|
||||||
<h2>Commands</h2>
|
|
||||||
|
|
||||||
<p class="small">Here you can set commands that are executed in the named events. You write one command
|
|
||||||
per line. If the event is related to files, such as before and after saving, the environment variable
|
|
||||||
<code>file</code> will be available with the path of the file.</p>
|
|
||||||
|
|
||||||
<template v-for="command in commands">
|
|
||||||
<h3>{{ capitalize(command.name) }}</h3>
|
|
||||||
<textarea v-model.trim="command.value"></textarea>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<p><input type="submit" value="Save"></p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState, mapMutations } from 'vuex'
|
|
||||||
import api from '@/utils/api'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'settings',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
commands: [],
|
|
||||||
plugins: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState([ 'user' ])
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
api.getCommands()
|
|
||||||
.then(commands => {
|
|
||||||
for (let key in commands) {
|
|
||||||
this.commands.push({
|
|
||||||
name: key,
|
|
||||||
value: commands[key].join('\n')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => { this.showError(error) })
|
|
||||||
|
|
||||||
api.getPlugins()
|
|
||||||
.then(plugins => {
|
|
||||||
for (let key in plugins) {
|
|
||||||
this.plugins.push(this.parsePlugin(key, plugins[key]))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => { this.showError(error) })
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapMutations([ 'showSuccess', 'showError' ]),
|
|
||||||
capitalize (name, where = '_') {
|
|
||||||
if (where === 'caps') where = /(?=[A-Z])/
|
|
||||||
let splitted = name.split(where)
|
|
||||||
name = ''
|
|
||||||
|
|
||||||
for (let i = 0; i < splitted.length; i++) {
|
|
||||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
|
||||||
}
|
|
||||||
|
|
||||||
return name.slice(0, -1)
|
|
||||||
},
|
|
||||||
saveCommands (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
let commands = {}
|
|
||||||
|
|
||||||
for (let command of this.commands) {
|
|
||||||
let value = command.value.split('\n')
|
|
||||||
if (value.length === 1 && value[0] === '') {
|
|
||||||
value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
commands[command.name] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
api.updateCommands(commands)
|
|
||||||
.then(() => { this.showSuccess('Commands updated!') })
|
|
||||||
.catch(error => { this.showError(error) })
|
|
||||||
},
|
|
||||||
savePlugin (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
let plugins = {}
|
|
||||||
|
|
||||||
for (let plugin of this.plugins) {
|
|
||||||
let p = {}
|
|
||||||
|
|
||||||
for (let field of plugin.fields) {
|
|
||||||
p[field.variable] = field.value
|
|
||||||
|
|
||||||
if (field.original === 'array') {
|
|
||||||
let val = field.value.split(' ')
|
|
||||||
if (val[0] === '') {
|
|
||||||
val.shift()
|
|
||||||
}
|
|
||||||
|
|
||||||
p[field.variable] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins[plugin.name] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(plugins)
|
|
||||||
|
|
||||||
api.updatePlugins(plugins)
|
|
||||||
.then(() => { this.showSuccess('Plugins settings updated!') })
|
|
||||||
.catch(error => { this.showError(error) })
|
|
||||||
},
|
|
||||||
parsePlugin (name, plugin) {
|
|
||||||
let obj = {
|
|
||||||
name: name,
|
|
||||||
fields: []
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let option of plugin) {
|
|
||||||
let value = option.value
|
|
||||||
|
|
||||||
let field = {
|
|
||||||
name: option.name,
|
|
||||||
variable: option.variable,
|
|
||||||
type: 'text',
|
|
||||||
original: 'text',
|
|
||||||
value: value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
field.original = 'array'
|
|
||||||
field.value = value.join(' ')
|
|
||||||
|
|
||||||
obj.fields.push(field)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (typeof value) {
|
|
||||||
case 'boolean':
|
|
||||||
field.type = 'checkbox'
|
|
||||||
field.original = 'boolean'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
obj.fields.push(field)
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,42 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<button @click="openSidebar" aria-label="Toggle sidebar" title="Toggle sidebar" class="action">
|
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||||
<i class="material-icons">menu</i>
|
<i class="material-icons">menu</i>
|
||||||
</button>
|
</button>
|
||||||
<img src="../assets/logo.svg" alt="File Manager">
|
<img src="../assets/logo.svg" alt="File Manager">
|
||||||
<search></search>
|
<search></search>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button @click="openSearch" aria-label="Search" title="Search" class="search-button action">
|
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
||||||
<i class="material-icons">search</i>
|
<i class="material-icons">search</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-show="showSaveButton" aria-label="Save" class="action" id="save-button">
|
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
|
||||||
<i class="material-icons" title="Save">save</i>
|
<i class="material-icons">save</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-for="plugin in plugins" :key="plugin.name">
|
<template v-if="staticGen.length > 0">
|
||||||
<button class="action"
|
<button v-show="showPublishButton" :aria-label="$t('buttons.publish')" :title="$t('buttons.publish')" class="action" id="publish-button">
|
||||||
v-for="action in plugin.header.visible"
|
<i class="material-icons">send</i>
|
||||||
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>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<button @click="openMore" id="more" aria-label="More" title="More" class="action">
|
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||||
<i class="material-icons">more_vert</i>
|
<i class="material-icons">more_vert</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
||||||
<div id="file-selection" v-if="isMobile && req.kind === 'listing'">
|
<div id="file-selection" v-if="isMobile && req.kind === 'listing'">
|
||||||
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
||||||
|
<share-button v-show="showRenameButton"></share-button>
|
||||||
<rename-button v-show="showRenameButton"></rename-button>
|
<rename-button v-show="showRenameButton"></rename-button>
|
||||||
<copy-button v-show="showMoveButton"></copy-button>
|
<copy-button v-show="showMoveButton"></copy-button>
|
||||||
<move-button v-show="showMoveButton"></move-button>
|
<move-button v-show="showMoveButton"></move-button>
|
||||||
@@ -46,34 +39,25 @@
|
|||||||
<!-- This buttons are shown on a dropdown on mobile phones -->
|
<!-- This buttons are shown on a dropdown on mobile phones -->
|
||||||
<div id="dropdown" :class="{ active: showMore }">
|
<div id="dropdown" :class="{ active: showMore }">
|
||||||
<div v-if="!isListing || !isMobile">
|
<div v-if="!isListing || !isMobile">
|
||||||
|
<share-button v-show="showRenameButton"></share-button>
|
||||||
<rename-button v-show="showRenameButton"></rename-button>
|
<rename-button v-show="showRenameButton"></rename-button>
|
||||||
<copy-button v-show="showMoveButton"></copy-button>
|
<copy-button v-show="showMoveButton"></copy-button>
|
||||||
<move-button v-show="showMoveButton"></move-button>
|
<move-button v-show="showMoveButton"></move-button>
|
||||||
<delete-button v-show="showDeleteButton"></delete-button>
|
<delete-button v-show="showDeleteButton"></delete-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="plugin in plugins" :key="plugin.name">
|
<template v-if="staticGen.length > 0">
|
||||||
<button class="action"
|
<schedule-button v-show="showPublishButton"></schedule-button>
|
||||||
v-for="action in plugin.header.hidden"
|
</template>
|
||||||
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>
|
|
||||||
|
|
||||||
<switch-button v-show="showSwitchButton"></switch-button>
|
<switch-button v-show="showSwitchButton"></switch-button>
|
||||||
<download-button v-show="showCommonButton"></download-button>
|
<download-button v-show="showCommonButton"></download-button>
|
||||||
<upload-button v-show="showUpload"></upload-button>
|
<upload-button v-show="showUpload"></upload-button>
|
||||||
<info-button v-show="showCommonButton"></info-button>
|
<info-button v-show="showCommonButton"></info-button>
|
||||||
|
|
||||||
<button v-show="showSelectButton" @click="openSelect" aria-label="Select multiple" class="action">
|
<button v-show="showSelectButton" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
|
||||||
<i class="material-icons">check_circle</i>
|
<i class="material-icons">check_circle</i>
|
||||||
<span>Select</span>
|
<span>{{ $t('buttons.select') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||||
@@ -91,8 +75,10 @@ import DownloadButton from './buttons/Download'
|
|||||||
import SwitchButton from './buttons/SwitchView'
|
import SwitchButton from './buttons/SwitchView'
|
||||||
import MoveButton from './buttons/Move'
|
import MoveButton from './buttons/Move'
|
||||||
import CopyButton from './buttons/Copy'
|
import CopyButton from './buttons/Copy'
|
||||||
|
import ScheduleButton from './buttons/Schedule'
|
||||||
|
import ShareButton from './buttons/Share'
|
||||||
import {mapGetters, mapState} from 'vuex'
|
import {mapGetters, mapState} from 'vuex'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -101,12 +87,14 @@ export default {
|
|||||||
Search,
|
Search,
|
||||||
InfoButton,
|
InfoButton,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
|
ShareButton,
|
||||||
RenameButton,
|
RenameButton,
|
||||||
DownloadButton,
|
DownloadButton,
|
||||||
CopyButton,
|
CopyButton,
|
||||||
UploadButton,
|
UploadButton,
|
||||||
SwitchButton,
|
SwitchButton,
|
||||||
MoveButton
|
MoveButton,
|
||||||
|
ScheduleButton
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
@@ -134,7 +122,7 @@ export default {
|
|||||||
'loading',
|
'loading',
|
||||||
'reload',
|
'reload',
|
||||||
'multiple',
|
'multiple',
|
||||||
'plugins'
|
'staticGen'
|
||||||
]),
|
]),
|
||||||
isMobile () {
|
isMobile () {
|
||||||
return this.width <= 736
|
return this.width <= 736
|
||||||
@@ -148,6 +136,9 @@ export default {
|
|||||||
showSaveButton () {
|
showSaveButton () {
|
||||||
return (this.req.kind === 'editor' && !this.loading)
|
return (this.req.kind === 'editor' && !this.loading)
|
||||||
},
|
},
|
||||||
|
showPublishButton () {
|
||||||
|
return (this.req.kind === 'editor' && !this.loading && this.user.allowPublish)
|
||||||
|
},
|
||||||
showSwitchButton () {
|
showSwitchButton () {
|
||||||
return this.req.kind === 'listing' && this.$route.name === 'Files' && !this.loading
|
return this.req.kind === 'listing' && this.$route.name === 'Files' && !this.loading
|
||||||
},
|
},
|
||||||
|
|||||||
22
assets/src/components/Languages.vue
Normal file
22
assets/src/components/Languages.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<select v-on:change="change" :value="selected">
|
||||||
|
<option value="en">{{ $t('languages.en') }}</option>
|
||||||
|
<option value="fr">{{ $t('languages.fr') }}</option>
|
||||||
|
<option value="pt">{{ $t('languages.pt') }}</option>
|
||||||
|
<option value="ja">{{ $t('languages.ja') }}</option>
|
||||||
|
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
||||||
|
<option value="zh-tw">{{ $t('languages.zhTW') }}</option>
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'languages',
|
||||||
|
props: [ 'selected' ],
|
||||||
|
methods: {
|
||||||
|
change (event) {
|
||||||
|
this.$emit('update:selected', event.target.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="login">
|
|
||||||
<form @submit="submit">
|
|
||||||
<img src="../assets/logo.svg" alt="File Manager">
|
|
||||||
<h1>File Manager</h1>
|
|
||||||
<div v-if="wrong" class="wrong">Wrong credentials</div>
|
|
||||||
<input type="text" v-model="username" placeholder="Username">
|
|
||||||
<input type="password" v-model="password" placeholder="Password">
|
|
||||||
<input type="submit" value="Login">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import auth from '@/utils/auth'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'login',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
wrong: false,
|
|
||||||
username: '',
|
|
||||||
password: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
submit: function (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
let redirect = this.$route.query.redirect
|
|
||||||
if (redirect === '' || redirect === undefined || redirect === null) {
|
|
||||||
redirect = '/files/'
|
|
||||||
}
|
|
||||||
|
|
||||||
auth.login(this.username, this.password)
|
|
||||||
.then(() => {
|
|
||||||
this.$router.push({ path: redirect })
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.wrong = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#login {
|
|
||||||
background: #fff;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login img {
|
|
||||||
width: 4em;
|
|
||||||
height: 4em;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login h1 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 2.5em;
|
|
||||||
margin: .4em 0 .67em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login form {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
max-width: 16em;
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login input {
|
|
||||||
width: 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin: .5em 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login .wrong {
|
|
||||||
background: #F44336;
|
|
||||||
color: #fff;
|
|
||||||
padding: .5em;
|
|
||||||
text-align: center;
|
|
||||||
animation: .2s opac forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes opac {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#login input[type="text"],
|
|
||||||
#login input[type="password"] {
|
|
||||||
padding: .5em 1em;
|
|
||||||
border: 1px solid #e9e9e9;
|
|
||||||
transition: .2s ease border;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login input[type="text"]:focus,
|
|
||||||
#login input[type="password"]:focus,
|
|
||||||
#login input[type="text"]:hover,
|
|
||||||
#login input[type="password"]:hover {
|
|
||||||
border-color: #9f9f9f;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<site-header></site-header>
|
|
||||||
<sidebar></sidebar>
|
|
||||||
<main>
|
|
||||||
<router-view v-on:css-updated="updateCSS"></router-view>
|
|
||||||
</main>
|
|
||||||
<prompts></prompts>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Search from './Search'
|
|
||||||
import Sidebar from './Sidebar'
|
|
||||||
import Prompts from './prompts/Prompts'
|
|
||||||
import SiteHeader from './Header'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'main',
|
|
||||||
components: {
|
|
||||||
Search,
|
|
||||||
Sidebar,
|
|
||||||
SiteHeader,
|
|
||||||
Prompts
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'$route': function () {
|
|
||||||
this.$store.commit('resetSelected')
|
|
||||||
this.$store.commit('multiple', false)
|
|
||||||
if (this.$store.state.show !== 'success') this.$store.commit('closeHovers')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.updateCSS()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateCSS () {
|
|
||||||
let css = this.$store.state.user.css
|
|
||||||
|
|
||||||
let style = document.querySelector('style[title="user-css"]')
|
|
||||||
if (style !== undefined && style !== null) {
|
|
||||||
style.parentElement.removeChild(style)
|
|
||||||
}
|
|
||||||
|
|
||||||
style = document.createElement('style')
|
|
||||||
style.title = 'user-css'
|
|
||||||
style.type = 'text/css'
|
|
||||||
style.appendChild(document.createTextNode(css))
|
|
||||||
document.head.appendChild(style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="dashboard">
|
|
||||||
<h1>Profile Settings</h1>
|
|
||||||
|
|
||||||
<ul v-if="user.admin">
|
|
||||||
<li><router-link to="/settings/global">Go to Global Settings</router-link></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<form @submit="changePassword">
|
|
||||||
<h2>Change Password</h2>
|
|
||||||
<p><input :class="passwordClass" type="password" placeholder="Your new password" v-model="password" name="password"></p>
|
|
||||||
<p><input :class="passwordClass" type="password" placeholder="Confirm your new password" v-model="passwordConf" name="password"></p>
|
|
||||||
<p><input type="submit" value="Change Password"></p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form @submit="updateCSS">
|
|
||||||
<h2>Custom Stylesheet</h2>
|
|
||||||
<textarea v-model="css" name="css"></textarea>
|
|
||||||
<p><input type="submit" value="Update"></p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState, mapMutations } from 'vuex'
|
|
||||||
import api from '@/utils/api'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'settings',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
password: '',
|
|
||||||
passwordConf: '',
|
|
||||||
css: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState([ 'user' ]),
|
|
||||||
passwordClass () {
|
|
||||||
if (this.password === '' && this.passwordConf === '') {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.password === this.passwordConf) {
|
|
||||||
return 'green'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'red'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.css = this.user.css
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapMutations([ 'showSuccess' ]),
|
|
||||||
changePassword (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (this.password !== this.passwordConf) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.updatePassword(this.password).then(() => {
|
|
||||||
this.showSuccess('Password updated!')
|
|
||||||
}).catch(e => {
|
|
||||||
this.$store.commit('showError', e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateCSS (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
api.updateCSS(this.css).then(() => {
|
|
||||||
this.$store.commit('setUserCSS', this.css)
|
|
||||||
this.$emit('css-updated')
|
|
||||||
this.showSuccess('Styles updated!')
|
|
||||||
}).catch(e => {
|
|
||||||
this.$store.commit('showError', e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
|
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
|
||||||
<div id="input">
|
<div id="input">
|
||||||
<button v-if="active" class="action" @click="close">
|
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
|
||||||
<i class="material-icons">arrow_back</i>
|
<i class="material-icons">arrow_back</i>
|
||||||
</button>
|
</button>
|
||||||
<i v-else class="material-icons">search</i>
|
<i v-else class="material-icons">search</i>
|
||||||
@@ -11,24 +11,70 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
:autofocus="active"
|
:autofocus="active"
|
||||||
v-model.trim="value"
|
v-model.trim="value"
|
||||||
aria-label="Write here to search"
|
:aria-label="$t('search.writeToSearch')"
|
||||||
:placeholder="placeholder">
|
:placeholder="placeholder">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="result">
|
<div id="result">
|
||||||
<div>
|
<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">
|
<ul v-else-if="search.length > 0">
|
||||||
<li v-for="s in search">
|
<li v-for="s in search">
|
||||||
<router-link @click.native="close" :to="'./' + s">./{{ s }}</router-link>
|
<router-link @click.native="close" :to="'./' + s.path">
|
||||||
|
<i v-if="s.dir" class="material-icons">folder</i>
|
||||||
|
<i v-else class="material-icons">insert_drive_file</i>
|
||||||
|
<span>./{{ s.path }}</span>
|
||||||
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul v-else-if="commands.length > 0">
|
<pre v-else-if="commands.length > 0">
|
||||||
<li v-for="c in commands">{{ c }}</li>
|
<template v-for="c in commands">{{ c }}</template>
|
||||||
</ul>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<p><i class="material-icons spin">autorenew</i></p>
|
<p id="renew"><i class="material-icons spin">autorenew</i></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -36,7 +82,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'search',
|
name: 'search',
|
||||||
@@ -62,6 +108,8 @@ export default {
|
|||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.body.style.overflow = 'auto'
|
||||||
|
this.reset()
|
||||||
this.$refs.input.blur()
|
this.$refs.input.blur()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +118,7 @@ export default {
|
|||||||
if (val === 'search') {
|
if (val === 'search') {
|
||||||
this.reload = false
|
this.reload = false
|
||||||
this.$refs.input.focus()
|
this.$refs.input.focus()
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -78,10 +127,10 @@ export default {
|
|||||||
// Placeholder value.
|
// Placeholder value.
|
||||||
placeholder: function () {
|
placeholder: function () {
|
||||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||||
return 'Search or execute a command...'
|
return this.$t('search.searchOrCommand')
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Search...'
|
return this.$t('search.search')
|
||||||
},
|
},
|
||||||
// The text that is shown on the results' box while
|
// The text that is shown on the results' box while
|
||||||
// there is no search result or command output to show.
|
// there is no search result or command output to show.
|
||||||
@@ -92,16 +141,16 @@ export default {
|
|||||||
|
|
||||||
if (this.value.length === 0) {
|
if (this.value.length === 0) {
|
||||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||||
return `Search or use one of your supported commands: ${this.user.commands.join(', ')}.`
|
return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.`
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Type and press enter to search.'
|
this.$t('search.type')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.supported() || !this.user.allowCommands) {
|
if (!this.supported() || !this.user.allowCommands) {
|
||||||
return 'Press enter to search.'
|
return this.$t('search.pressToSearch')
|
||||||
} else {
|
} else {
|
||||||
return 'Press enter to execute.'
|
return this.$t('search.pressToExecute')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -119,19 +168,19 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// Sets the search to active.
|
// Sets the search to active.
|
||||||
open: function (event) {
|
open (event) {
|
||||||
this.$store.commit('showHover', 'search')
|
this.$store.commit('showHover', 'search')
|
||||||
},
|
},
|
||||||
// Closes the search and prevents the event
|
// Closes the search and prevents the event
|
||||||
// of propagating so it doesn't trigger the
|
// of propagating so it doesn't trigger the
|
||||||
// click event on #search.
|
// click event on #search.
|
||||||
close: function (event) {
|
close (event) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.$store.commit('closeHovers')
|
this.$store.commit('closeHovers')
|
||||||
},
|
},
|
||||||
// Checks if the current input is a supported command.
|
// Checks if the current input is a supported command.
|
||||||
supported: function () {
|
supported () {
|
||||||
let pieces = this.value.split(' ')
|
let pieces = this.value.split(' ')
|
||||||
|
|
||||||
for (let i = 0; i < this.user.commands.length; i++) {
|
for (let i = 0; i < this.user.commands.length; i++) {
|
||||||
@@ -142,11 +191,24 @@ export default {
|
|||||||
|
|
||||||
return false
|
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
|
// When the user presses a key, if it is ESC
|
||||||
// then it will close the search box. Otherwise,
|
// then it will close the search box. Otherwise,
|
||||||
// it will set the search box to active and clean
|
// it will set the search box to active and clean
|
||||||
// the search results, as well as commands'.
|
// the search results, as well as commands'.
|
||||||
keyup: function (event) {
|
keyup (event) {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
this.close(event)
|
this.close(event)
|
||||||
return
|
return
|
||||||
@@ -156,7 +218,7 @@ export default {
|
|||||||
this.commands.length = 0
|
this.commands.length = 0
|
||||||
},
|
},
|
||||||
// Submits the input to the server and sets ongoing to true.
|
// Submits the input to the server and sets ongoing to true.
|
||||||
submit: function (event) {
|
submit (event) {
|
||||||
this.ongoing = true
|
this.ongoing = true
|
||||||
|
|
||||||
let path = this.$route.path
|
let path = this.$route.path
|
||||||
@@ -184,10 +246,12 @@ export default {
|
|||||||
// In case of being a search.
|
// In case of being a search.
|
||||||
api.search(path, this.value,
|
api.search(path, this.value,
|
||||||
(event) => {
|
(event) => {
|
||||||
let url = event.data
|
let response = JSON.parse(event.data)
|
||||||
if (url[0] === '/') url = url.substring(1)
|
if (response.path[0] === '/') {
|
||||||
|
response.path = response.path.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
this.search.push(url)
|
this.search.push(response)
|
||||||
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
this.scrollable.scrollTop = this.scrollable.scrollHeight
|
||||||
},
|
},
|
||||||
(event) => {
|
(event) => {
|
||||||
|
|||||||
@@ -1,45 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav :class="{active}">
|
<nav :class="{active}">
|
||||||
<router-link class="action" to="/files/" aria-label="My Files" title="My Files">
|
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
|
||||||
<i class="material-icons">folder</i>
|
<i class="material-icons">folder</i>
|
||||||
<span>My Files</span>
|
<span>{{ $t('sidebar.myFiles') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div v-if="user.allowNew">
|
<div v-if="user.allowNew">
|
||||||
<button @click="$store.commit('showHover', 'newDir')" aria-label="New directory" title="New directory" class="action">
|
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
|
||||||
<i class="material-icons">create_new_folder</i>
|
<i class="material-icons">create_new_folder</i>
|
||||||
<span>New folder</span>
|
<span>{{ $t('sidebar.newFolder') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="$store.commit('showHover', 'newFile')" aria-label="New file" title="New file" class="action">
|
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
|
||||||
<i class="material-icons">note_add</i>
|
<i class="material-icons">note_add</i>
|
||||||
<span>New file</span>
|
<span>{{ $t('sidebar.newFile') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="plugin in plugins" :key="plugin.name">
|
<div v-if="staticGen.length > 0">
|
||||||
<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">
|
<router-link to="/files/settings"
|
||||||
<i class="material-icons">{{ action.icon }}</i>
|
:aria-label="$t('sidebar.siteSettings')"
|
||||||
<span>{{ action.name }}</span>
|
:title="$t('sidebar.siteSettings')"
|
||||||
</button>
|
class="action">
|
||||||
</div>
|
<i class="material-icons">settings</i>
|
||||||
|
<span>{{ $t('sidebar.siteSettings') }}</span>
|
||||||
<div>
|
|
||||||
<router-link class="action" to="/settings" aria-label="Settings" title="Settings">
|
|
||||||
<i class="material-icons">settings_applications</i>
|
|
||||||
<span>Settings</span>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<button @click="logout" class="action" id="logout" aria-label="Log out" title="Logout">
|
<template v-if="staticGen === 'hugo'">
|
||||||
|
<button class="action"
|
||||||
|
:aria-label="$t('sidebar.hugoNew')"
|
||||||
|
:title="$t('sidebar.hugoNew')"
|
||||||
|
v-if="user.allowNew"
|
||||||
|
@click="$store.commit('showHover', 'new-archetype')">
|
||||||
|
<i class="material-icons">merge_type</i>
|
||||||
|
<span>{{ $t('sidebar.hugoNew') }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<button class="action"
|
||||||
|
:aria-label="$t('sidebar.preview')"
|
||||||
|
:title="$t('sidebar.preview')"
|
||||||
|
@click="preview">
|
||||||
|
<i class="material-icons">remove_red_eye</i>
|
||||||
|
<span>{{ $t('sidebar.preview') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!$store.state.noAuth">
|
||||||
|
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
||||||
|
<i class="material-icons">settings_applications</i>
|
||||||
|
<span>{{ $t('sidebar.settings') }}</span>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<button @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
||||||
<i class="material-icons">exit_to_app</i>
|
<i class="material-icons">exit_to_app</i>
|
||||||
<span>Logout</span>
|
<span>{{ $t('sidebar.logout') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="credits">
|
<p class="credits">
|
||||||
<span>Served with <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
|
<span><a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a> v{{ version }}</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>
|
||||||
<span><a @click="help">Help</a></span>
|
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
@@ -47,31 +68,22 @@
|
|||||||
<script>
|
<script>
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
import auth from '@/utils/auth'
|
import auth from '@/utils/auth'
|
||||||
import buttons from '@/utils/buttons'
|
|
||||||
import api from '@/utils/api'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'sidebar',
|
name: 'sidebar',
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
pluginData: {
|
|
||||||
api,
|
|
||||||
buttons,
|
|
||||||
'store': this.$store,
|
|
||||||
'router': this.$router
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['user', 'plugins']),
|
...mapState(['user', 'staticGen', 'version']),
|
||||||
active () {
|
active () {
|
||||||
return this.$store.state.show === 'sidebar'
|
return this.$store.state.show === 'sidebar'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
help: function () {
|
help () {
|
||||||
this.$store.commit('showHover', 'help')
|
this.$store.commit('showHover', 'help')
|
||||||
},
|
},
|
||||||
|
preview () {
|
||||||
|
window.open(this.$store.state.baseURL + '/preview/')
|
||||||
|
},
|
||||||
logout: auth.logout
|
logout: auth.logout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<form @submit="save" class="dashboard">
|
|
||||||
<h1 v-if="id === 0">New User</h1>
|
|
||||||
<h1 v-else>User {{ username }}</h1>
|
|
||||||
|
|
||||||
<p><label for="username">Username</label><input type="text" v-model="username" id="username"></p>
|
|
||||||
<p><label for="password">Password</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
|
|
||||||
<p><label for="scope">Scope</label><input type="text" v-model="filesystem" id="scope"></p>
|
|
||||||
|
|
||||||
<h2>Permissions</h2>
|
|
||||||
|
|
||||||
<p class="small">You can set the user to be an administrator or choose the permissions individually.
|
|
||||||
If you select "Administrator", all of the other options will be automatically checked.
|
|
||||||
The management of users remains a privilege of an administrator.</p>
|
|
||||||
|
|
||||||
<p><input type="checkbox" v-model="admin"> Administrator</p>
|
|
||||||
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> Create new files and directories</p>
|
|
||||||
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> Edit, rename and delete files or directories.</p>
|
|
||||||
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> Execute commands</p>
|
|
||||||
<p v-for="(value, key) in permissions" :key="key">
|
|
||||||
<input type="checkbox" :disabled="admin" v-model="permissions[key]"> {{ capitalize(key) }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>Commands</h3>
|
|
||||||
|
|
||||||
<p class="small">A space separated list with the available commands for this user. Example: <i>git svn hg</i>.</p>
|
|
||||||
|
|
||||||
<input type="text" v-model.trim="commands">
|
|
||||||
|
|
||||||
<h2>Rules</h2>
|
|
||||||
|
|
||||||
<p class="small">Here you can define a set of allow and disallow rules for this specific user. The blocked files won't
|
|
||||||
show up in the listings and they won't be accessible to the user. We support regex and paths relative to
|
|
||||||
the user's scope.</p>
|
|
||||||
|
|
||||||
<p class="small">Each rule goes in one different line and must start with the keyword <code>allow</code> or <code>disallow</code>.
|
|
||||||
Then you should write <code>regex</code> if you are using a regular expression and then the expression or the path.</p>
|
|
||||||
|
|
||||||
<p class="small"><strong>Examples</strong></p>
|
|
||||||
|
|
||||||
<ul class="small">
|
|
||||||
<li><code>disallow regex \\/\\..+</code> - prevents the access to any dot file (such as .git, .gitignore) in every folder.</li>
|
|
||||||
<li><code>disallow /Caddyfile</code> - blocks the access to the file named <i>Caddyfile</i> on the root of the scope</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<textarea v-model.trim="rules"></textarea>
|
|
||||||
|
|
||||||
<h2>Custom Stylesheet</h2>
|
|
||||||
|
|
||||||
<textarea name="css"></textarea>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete">Delete</button>
|
|
||||||
<input type="submit" value="Save">
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div v-if="$store.state.show === 'deleteUser'" class="prompt">
|
|
||||||
<h3>Delete User</h3>
|
|
||||||
<p>Are you sure you want to delete this user?</p>
|
|
||||||
<div>
|
|
||||||
<button @click="deleteUser" autofocus>Delete</button>
|
|
||||||
<button @click="closeHovers" class="cancel">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapMutations } from 'vuex'
|
|
||||||
import api from '@/utils/api'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'user',
|
|
||||||
data: () => {
|
|
||||||
return {
|
|
||||||
id: 0,
|
|
||||||
admin: false,
|
|
||||||
allowNew: false,
|
|
||||||
allowEdit: false,
|
|
||||||
allowCommands: false,
|
|
||||||
permissions: {},
|
|
||||||
password: '',
|
|
||||||
username: '',
|
|
||||||
filesystem: '',
|
|
||||||
rules: '',
|
|
||||||
css: '',
|
|
||||||
commands: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
passwordPlaceholder () {
|
|
||||||
if (this.$route.path === '/users/new') return ''
|
|
||||||
return '(leave blank to avoid changes)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'$route': 'fetchData',
|
|
||||||
admin: function () {
|
|
||||||
if (!this.admin) return
|
|
||||||
this.allowCommands = true
|
|
||||||
this.allowEdit = true
|
|
||||||
this.allowNew = true
|
|
||||||
for (let key in this.permissions) {
|
|
||||||
this.permissions[key] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapMutations(['closeHovers']),
|
|
||||||
fetchData () {
|
|
||||||
let user = this.$route.params[0]
|
|
||||||
|
|
||||||
if (this.$route.path === '/users/new') {
|
|
||||||
user = 'base'
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getUser(user).then(user => {
|
|
||||||
this.id = user.ID
|
|
||||||
this.admin = user.admin
|
|
||||||
this.allowCommands = user.allowCommands
|
|
||||||
this.allowNew = user.allowNew
|
|
||||||
this.allowEdit = user.allowEdit
|
|
||||||
this.filesystem = user.filesystem
|
|
||||||
this.username = user.username
|
|
||||||
this.commands = user.commands.join(' ')
|
|
||||||
this.css = user.css
|
|
||||||
this.permissions = user.permissions
|
|
||||||
|
|
||||||
for (let rule of user.rules) {
|
|
||||||
if (rule.allow) {
|
|
||||||
this.rules += 'allow '
|
|
||||||
} else {
|
|
||||||
this.rules += 'disallow '
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.regex) {
|
|
||||||
this.rules += 'regex ' + rule.regexp.raw
|
|
||||||
} else {
|
|
||||||
this.rules += rule.path
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rules += '\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rules = this.rules.trim()
|
|
||||||
}).catch(() => {
|
|
||||||
this.$router.push({ path: '/users/new' })
|
|
||||||
})
|
|
||||||
},
|
|
||||||
capitalize (name) {
|
|
||||||
let splitted = name.split(/(?=[A-Z])/)
|
|
||||||
name = ''
|
|
||||||
|
|
||||||
for (let i = 0; i < splitted.length; i++) {
|
|
||||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
|
||||||
}
|
|
||||||
|
|
||||||
return name.slice(0, -1)
|
|
||||||
},
|
|
||||||
reset () {
|
|
||||||
this.id = 0
|
|
||||||
this.admin = false
|
|
||||||
this.allowNew = false
|
|
||||||
this.allowEdit = false
|
|
||||||
this.permissins = {}
|
|
||||||
this.allowCommands = false
|
|
||||||
this.password = ''
|
|
||||||
this.username = ''
|
|
||||||
this.filesystem = ''
|
|
||||||
this.rules = ''
|
|
||||||
this.css = ''
|
|
||||||
this.commands = ''
|
|
||||||
},
|
|
||||||
deletePrompt (event) {
|
|
||||||
this.$store.commit('showHover', 'deleteUser')
|
|
||||||
},
|
|
||||||
deleteUser (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
api.deleteUser(this.id).then(location => {
|
|
||||||
this.$router.push({ path: '/users' })
|
|
||||||
this.$store.commit('showSuccess', 'User deleted!')
|
|
||||||
}).catch(e => {
|
|
||||||
this.$store.commit('showError', e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
save (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
let user = this.parseForm()
|
|
||||||
|
|
||||||
if (this.$route.path === '/users/new') {
|
|
||||||
api.newUser(user).then(location => {
|
|
||||||
this.$router.push({ path: location })
|
|
||||||
this.$store.commit('showSuccess', 'User created!')
|
|
||||||
}).catch(e => {
|
|
||||||
this.$store.commit('showError', e)
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.updateUser(user).then(location => {
|
|
||||||
this.$store.commit('showSuccess', 'User updated!')
|
|
||||||
}).catch(e => {
|
|
||||||
this.$store.commit('showError', e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
parseForm () {
|
|
||||||
let user = {
|
|
||||||
ID: this.id,
|
|
||||||
username: this.username,
|
|
||||||
password: this.password,
|
|
||||||
filesystem: this.filesystem,
|
|
||||||
admin: this.admin,
|
|
||||||
allowCommands: this.allowCommands,
|
|
||||||
allowNew: this.allowNew,
|
|
||||||
allowEdit: this.allowEdit,
|
|
||||||
permissions: this.permissions,
|
|
||||||
css: this.css,
|
|
||||||
commands: this.commands.split(' '),
|
|
||||||
rules: []
|
|
||||||
}
|
|
||||||
|
|
||||||
let rules = this.rules.split('\n')
|
|
||||||
|
|
||||||
for (let rawRule of rules) {
|
|
||||||
let rule = {
|
|
||||||
allow: true,
|
|
||||||
path: '',
|
|
||||||
regex: false,
|
|
||||||
regexp: {
|
|
||||||
raw: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rawRule = rawRule.split(' ')
|
|
||||||
|
|
||||||
// Skip a malformed rule
|
|
||||||
if (rawRule.length < 2) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip a malformed rule
|
|
||||||
if (rawRule[0] !== 'allow' && rawRule[0] !== 'disallow') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.allow = (rawRule[0] === 'allow')
|
|
||||||
rawRule.shift()
|
|
||||||
|
|
||||||
if (rawRule[0] === 'regex') {
|
|
||||||
rule.regex = true
|
|
||||||
rawRule.shift()
|
|
||||||
rule.regexp.raw = rawRule.join(' ')
|
|
||||||
} else {
|
|
||||||
rule.path = rawRule.join(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
user.rules.push(rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="dashboard">
|
|
||||||
<h1>Users <router-link to="/users/new"><button>New</button></router-link></h1>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Admin</th>
|
|
||||||
<th>Scope</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr v-for="user in users">
|
|
||||||
<td>{{ user.username }}</td>
|
|
||||||
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
|
|
||||||
<td>{{ user.filesystem }}</td>
|
|
||||||
<td><router-link :to="'/users/' + user.ID"><i class="material-icons">mode_edit</i></router-link></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import api from '@/utils/api'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'users',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
users: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
api.getUsers().then(users => {
|
|
||||||
this.users = users
|
|
||||||
}).catch(error => {
|
|
||||||
this.$store.commit('showError', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button @click="show" aria-label="Copy" title="Copy" class="action" id="copy-button">
|
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
|
||||||
<i class="material-icons">content_copy</i>
|
<i class="material-icons">content_copy</i>
|
||||||
<span>Copy file</span>
|
<span>{{ $t('buttons.copyFile') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button @click="show" aria-label="Delete" title="Delete" class="action" id="delete-button">
|
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
|
||||||
<i class="material-icons">delete</i>
|
<i class="material-icons">delete</i>
|
||||||
<span>Delete</span>
|
<span>{{ $t('buttons.delete') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<button @click="download" aria-label="Download" title="Download" id="download-button" class="action">
|
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
|
||||||
<i class="material-icons">file_download</i>
|
<i class="material-icons">file_download</i>
|
||||||
<span>Download</span>
|
<span>{{ $t('buttons.download') }}</span>
|
||||||
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
|
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapGetters, mapState} from 'vuex'
|
import {mapGetters, mapState} from 'vuex'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'download-button',
|
name: 'download-button',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button title="Info" aria-label="Info" class="action" @click="show">
|
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
|
||||||
<i class="material-icons">info</i>
|
<i class="material-icons">info</i>
|
||||||
<span>Info</span>
|
<span>{{ $t('buttons.info') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button @click="show" aria-label="Move" title="Move" class="action" id="move-button">
|
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
|
||||||
<i class="material-icons">forward</i>
|
<i class="material-icons">forward</i>
|
||||||
<span>Move file</span>
|
<span>{{ $t('buttons.moveFile') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button @click="show" aria-label="Rename" title="Rename" class="action" id="rename-button">
|
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
|
||||||
<i class="material-icons">mode_edit</i>
|
<i class="material-icons">mode_edit</i>
|
||||||
<span>Rename</span>
|
<span>{{ $t('buttons.rename') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
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>
|
||||||
@@ -1,32 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<button @click="change" aria-label="Switch View" title="Switch View" class="action" id="switch-view-button">
|
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
|
||||||
<i class="material-icons">{{ icon() }}</i>
|
<i class="material-icons">{{ icon }}</i>
|
||||||
<span>Switch view</span>
|
<span>{{ $t('buttons.switchView') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapState, mapMutations } from 'vuex'
|
||||||
|
import { updateUser } from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'switch-button',
|
name: 'switch-button',
|
||||||
|
computed: {
|
||||||
|
...mapState(['user']),
|
||||||
|
icon: function () {
|
||||||
|
if (this.user.viewMode === 'mosaic') return 'view_list'
|
||||||
|
return 'view_module'
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapMutations(['updateUser']),
|
||||||
change: function (event) {
|
change: function (event) {
|
||||||
// If we are on mobile we should close the dropdown.
|
// If we are on mobile we should close the dropdown.
|
||||||
this.$store.commit('closeHovers')
|
this.$store.commit('closeHovers')
|
||||||
|
|
||||||
let display = 'mosaic'
|
let user = {...this.user}
|
||||||
|
user.viewMode = (this.icon === 'view_list') ? 'list' : 'mosaic'
|
||||||
|
|
||||||
if (this.$store.state.req.display === 'mosaic') {
|
updateUser(user, 'partial').then(() => {
|
||||||
display = 'list'
|
this.updateUser({ viewMode: user.viewMode })
|
||||||
}
|
}).catch(this.$showError)
|
||||||
|
|
||||||
this.$store.commit('listingDisplay', display)
|
|
||||||
let path = this.$store.state.baseURL
|
|
||||||
if (path === '') path = '/'
|
|
||||||
document.cookie = `display=${display}; max-age=31536000; path=${path}`
|
|
||||||
},
|
|
||||||
icon: function () {
|
|
||||||
if (this.$store.state.req.display === 'mosaic') return 'view_list'
|
|
||||||
return 'view_module'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button @click="upload" aria-label="Upload" title="Upload" class="action" id="upload-button">
|
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
|
||||||
<i class="material-icons">file_upload</i>
|
<i class="material-icons">file_upload</i>
|
||||||
<span>Upload</span>
|
<span>{{ $t('buttons.upload') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<form id="editor" :class="req.language">
|
<form id="editor" :class="req.language">
|
||||||
<div v-if="hasMetadata" id="metadata">
|
<div v-if="hasMetadata" id="metadata">
|
||||||
<h2>Metadata</h2>
|
<h2>{{ $t('files.metadata') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 v-if="hasMetadata">Body</h2>
|
<h2 v-if="hasMetadata">{{ $t('files.body') }}</h2>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import CodeMirror from '@/utils/codemirror'
|
import CodeMirror from '@/utils/codemirror'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'editor',
|
name: 'editor',
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['req']),
|
...mapState(['req', 'schedule']),
|
||||||
hasMetadata: function () {
|
hasMetadata: function () {
|
||||||
return (this.req.metadata !== undefined && this.req.metadata !== null)
|
return (this.req.metadata !== undefined && this.req.metadata !== null)
|
||||||
}
|
}
|
||||||
@@ -32,10 +32,20 @@ export default {
|
|||||||
created () {
|
created () {
|
||||||
window.addEventListener('keydown', this.keyEvent)
|
window.addEventListener('keydown', this.keyEvent)
|
||||||
document.getElementById('save-button').addEventListener('click', this.save)
|
document.getElementById('save-button').addEventListener('click', this.save)
|
||||||
|
|
||||||
|
let publish = document.getElementById('publish-button')
|
||||||
|
if (publish !== null) {
|
||||||
|
publish.addEventListener('click', this.publish)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
window.removeEventListener('keydown', this.keyEvent)
|
window.removeEventListener('keydown', this.keyEvent)
|
||||||
document.getElementById('save-button').removeEventListener('click', this.save)
|
document.getElementById('save-button').removeEventListener('click', this.save)
|
||||||
|
|
||||||
|
let publish = document.getElementById('publish-button')
|
||||||
|
if (publish !== null) {
|
||||||
|
publish.removeEventListener('click', this.publish)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
if (this.req.content === undefined || this.req.content === null) {
|
if (this.req.content === undefined || this.req.content === null) {
|
||||||
@@ -102,28 +112,32 @@ export default {
|
|||||||
this.metalang = 'toml'
|
this.metalang = 'toml'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Publishes the file.
|
||||||
|
publish (event) {
|
||||||
|
this.save(event, true)
|
||||||
|
},
|
||||||
// Saves the file.
|
// Saves the file.
|
||||||
save () {
|
save (event, regenerate = false) {
|
||||||
buttons.loading('save')
|
let button = regenerate ? 'publish' : 'save'
|
||||||
|
if (this.schedule !== '') button = 'schedule'
|
||||||
let content = this.content.getValue()
|
let content = this.content.getValue()
|
||||||
|
buttons.loading(button)
|
||||||
|
|
||||||
if (this.hasMetadata) {
|
if (this.hasMetadata) {
|
||||||
content = this.metadata.getValue() + '\n\n' + content
|
content = this.metadata.getValue() + '\n\n' + content
|
||||||
}
|
}
|
||||||
|
|
||||||
api.put(this.$route.path, content)
|
api.put(this.$route.path, content, regenerate, this.schedule)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('save')
|
buttons.success(button)
|
||||||
|
this.$store.commit('setSchedule', '')
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('save')
|
buttons.done(button)
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
|
this.$store.commit('setSchedule', '')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -2,37 +2,51 @@
|
|||||||
<div v-if="(req.numDirs + req.numFiles) == 0">
|
<div v-if="(req.numDirs + req.numFiles) == 0">
|
||||||
<h2 class="message">
|
<h2 class="message">
|
||||||
<i class="material-icons">sentiment_dissatisfied</i>
|
<i class="material-icons">sentiment_dissatisfied</i>
|
||||||
<span>It feels lonely here...</span>
|
<span>{{ $t('files.lonely') }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
|
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||||
</div>
|
</div>
|
||||||
<div v-else id="listing"
|
<div v-else id="listing"
|
||||||
:class="req.display"
|
:class="user.viewMode"
|
||||||
@drop="drop"
|
|
||||||
@dragenter="dragEnter"
|
@dragenter="dragEnter"
|
||||||
@dragend="dragEnd">
|
@dragend="dragEnd">
|
||||||
<div>
|
<div>
|
||||||
<div class="item header">
|
<div class="item header">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div>
|
<div>
|
||||||
<p :class="{ active: nameSorted }" class="name" @click="sort('name')">
|
<p :class="{ active: nameSorted }" class="name"
|
||||||
<span>Name</span>
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="sort('name')"
|
||||||
|
:title="$t('files.sortByName')"
|
||||||
|
:aria-label="$t('files.sortByName')">
|
||||||
|
<span>{{ $t('files.name') }}</span>
|
||||||
<i class="material-icons">{{ nameIcon }}</i>
|
<i class="material-icons">{{ nameIcon }}</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p :class="{ active: sizeSorted }" class="size" @click="sort('size')">
|
<p :class="{ active: sizeSorted }" class="size"
|
||||||
<span>Size</span>
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="sort('size')"
|
||||||
|
:title="$t('files.sortBySize')"
|
||||||
|
:aria-label="$t('files.sortBySize')">
|
||||||
|
<span>{{ $t('files.size') }}</span>
|
||||||
<i class="material-icons">{{ sizeIcon }}</i>
|
<i class="material-icons">{{ sizeIcon }}</i>
|
||||||
</p>
|
</p>
|
||||||
<p :class="{ active: modifiedSorted }" class="modified" @click="sort('modified')">
|
<p :class="{ active: modifiedSorted }" class="modified"
|
||||||
<span>Last modified</span>
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="sort('modified')"
|
||||||
|
:title="$t('files.sortByLastModified')"
|
||||||
|
:aria-label="$t('files.sortByLastModified')">
|
||||||
|
<span>{{ $t('files.lastModified') }}</span>
|
||||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 v-if="req.numDirs > 0">Folders</h2>
|
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
|
||||||
<div v-if="req.numDirs > 0">
|
<div v-if="req.numDirs > 0">
|
||||||
<item v-for="(item, index) in req.items"
|
<item v-for="(item, index) in req.items"
|
||||||
v-if="item.isDir"
|
v-if="item.isDir"
|
||||||
@@ -47,7 +61,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 v-if="req.numFiles > 0">Files</h2>
|
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
|
||||||
<div v-if="req.numFiles > 0">
|
<div v-if="req.numFiles > 0">
|
||||||
<item v-for="(item, index) in req.items"
|
<item v-for="(item, index) in req.items"
|
||||||
v-if="!item.isDir"
|
v-if="!item.isDir"
|
||||||
@@ -62,12 +76,12 @@
|
|||||||
</item>
|
</item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
|
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||||
|
|
||||||
<div v-show="$store.state.multiple" :class="{ active: $store.state.multiple }" id="multiple-selection">
|
<div v-show="$store.state.multiple" :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||||
<p>Multiple selection enabled</p>
|
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||||
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" title="Clear" aria-label="Clear" class="action">
|
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
|
||||||
<i class="material-icons" title="Clear">clear</i>
|
<i class="material-icons">clear</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,14 +91,14 @@
|
|||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
import Item from './ListingItem'
|
import Item from './ListingItem'
|
||||||
import css from '@/utils/css'
|
import css from '@/utils/css'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'listing',
|
name: 'listing',
|
||||||
components: { Item },
|
components: { Item },
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['req', 'selected']),
|
...mapState(['req', 'selected', 'user']),
|
||||||
nameSorted () {
|
nameSorted () {
|
||||||
return (this.req.sort === 'name')
|
return (this.req.sort === 'name')
|
||||||
},
|
},
|
||||||
@@ -196,17 +210,13 @@ export default {
|
|||||||
if (this.$store.state.clipboard.key === 'x') {
|
if (this.$store.state.clipboard.key === 'x') {
|
||||||
api.move(items).then(() => {
|
api.move(items).then(() => {
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
}).catch(error => {
|
}).catch(this.$showError)
|
||||||
this.$store.commit('showError', error)
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
api.copy(items).then(() => {
|
api.copy(items).then(() => {
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
}).catch(error => {
|
}).catch(this.$showError)
|
||||||
this.$store.commit('showError', error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
resizeEvent () {
|
resizeEvent () {
|
||||||
// Update the columns size based on the window width.
|
// Update the columns size based on the window width.
|
||||||
@@ -215,7 +225,7 @@ export default {
|
|||||||
if (columns === 0) columns = 1
|
if (columns === 0) columns = 1
|
||||||
items.style.width = `calc(${100 / columns}% - 1em)`
|
items.style.width = `calc(${100 / columns}% - 1em)`
|
||||||
},
|
},
|
||||||
dragEnter: function (event) {
|
dragEnter (event) {
|
||||||
// When the user starts dragging an item, put every
|
// When the user starts dragging an item, put every
|
||||||
// file on the listing with 50% opacity.
|
// file on the listing with 50% opacity.
|
||||||
let items = document.getElementsByClassName('item')
|
let items = document.getElementsByClassName('item')
|
||||||
@@ -224,61 +234,117 @@ export default {
|
|||||||
file.style.opacity = 0.5
|
file.style.opacity = 0.5
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dragEnd: function (event) {
|
dragEnd (event) {
|
||||||
this.resetOpacity()
|
this.resetOpacity()
|
||||||
},
|
},
|
||||||
drop: function (event) {
|
drop: function (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
this.resetOpacity()
|
||||||
|
|
||||||
let dt = event.dataTransfer
|
let dt = event.dataTransfer
|
||||||
let files = dt.files
|
let files = dt.files
|
||||||
let el = event.target
|
let el = event.target
|
||||||
|
|
||||||
|
if (files.length <= 0) return
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
if (el !== null && !el.classList.contains('item')) {
|
if (el !== null && !el.classList.contains('item')) {
|
||||||
el = el.parentElement
|
el = el.parentElement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files.length > 0) {
|
let base = ''
|
||||||
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
|
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
|
||||||
this.handleFiles(files, el.querySelector('.name').innerHTML + '/')
|
base = el.querySelector('.name').innerHTML + '/'
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleFiles(files, '')
|
|
||||||
} else {
|
|
||||||
this.resetOpacity()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (base !== '') {
|
||||||
|
api.fetch(this.$route.path + base)
|
||||||
|
.then(req => {
|
||||||
|
this.checkConflict(files, req.items, base)
|
||||||
|
})
|
||||||
|
.catch(this.$showError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkConflict(files, this.req.items, base)
|
||||||
},
|
},
|
||||||
uploadInput: function (event) {
|
checkConflict (files, items, base) {
|
||||||
this.handleFiles(event.currentTarget.files, '')
|
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')
|
let items = document.getElementsByClassName('item')
|
||||||
|
|
||||||
Array.from(items).forEach(file => {
|
Array.from(items).forEach(file => {
|
||||||
file.style.opacity = 1
|
file.style.opacity = 1
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleFiles: function (files, base) {
|
handleFiles (files, base, overwrite = false) {
|
||||||
this.resetOpacity()
|
|
||||||
|
|
||||||
buttons.loading('upload')
|
buttons.loading('upload')
|
||||||
let promises = []
|
let promises = []
|
||||||
|
let progress = new Array(files.length).fill(0)
|
||||||
|
|
||||||
for (let file of files) {
|
let onupload = (id) => (event) => {
|
||||||
promises.push(api.post(this.$route.path + base + file.name, file))
|
progress[id] = (event.loaded / event.total) * 100
|
||||||
|
|
||||||
|
let sum = 0
|
||||||
|
for (let i = 0; i < progress.length; i++) {
|
||||||
|
sum += progress[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.commit('setProgress', Math.ceil(sum / progress.length))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
let file = files[i]
|
||||||
|
promises.push(api.post(this.$route.path + base + file.name, file, overwrite, onupload(i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
let finish = () => {
|
||||||
|
buttons.success('upload')
|
||||||
|
this.$store.commit('setProgress', 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('upload')
|
finish()
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('upload')
|
finish()
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="item"
|
<div class="item"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="dragStart"
|
@dragstart="dragStart"
|
||||||
@dragover="dragOver"
|
@dragover="dragOver"
|
||||||
@@ -7,6 +9,8 @@
|
|||||||
@click="click"
|
@click="click"
|
||||||
@dblclick="open"
|
@dblclick="open"
|
||||||
@touchstart="touchstart"
|
@touchstart="touchstart"
|
||||||
|
:data-dir="isDir"
|
||||||
|
:aria-label="name"
|
||||||
:aria-selected="isSelected">
|
:aria-selected="isSelected">
|
||||||
<div>
|
<div>
|
||||||
<i class="material-icons">{{ icon }}</i>
|
<i class="material-icons">{{ icon }}</i>
|
||||||
@@ -29,7 +33,7 @@
|
|||||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||||
import filesize from 'filesize'
|
import filesize from 'filesize'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'item',
|
name: 'item',
|
||||||
@@ -105,21 +109,24 @@ export default {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(this.$showError)
|
||||||
this.$store.commit('showError', error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
click: function (event) {
|
click: function (event) {
|
||||||
if (this.selectedCount !== 0) event.preventDefault()
|
if (this.selectedCount !== 0) event.preventDefault()
|
||||||
if (this.$store.state.selected.indexOf(this.index) === -1) {
|
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||||
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
|
||||||
|
|
||||||
this.addSelected(this.index)
|
|
||||||
} else {
|
|
||||||
this.removeSelected(this.index)
|
this.removeSelected(this.index)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
if (event.shiftKey && this.selected.length === 1) {
|
||||||
|
let fi = (this.index > this.selected[0]) ? this.selected[0] : this.index
|
||||||
|
let la = (this.index > this.selected[0]) ? this.index : this.selected[0]
|
||||||
|
for (; fi <= la; fi++) this.addSelected(fi)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||||
|
this.addSelected(this.index)
|
||||||
},
|
},
|
||||||
touchstart (event) {
|
touchstart (event) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="previewer">
|
<div id="previewer">
|
||||||
<div class="bar">
|
<div class="bar">
|
||||||
<button @click="back" class="action" aria-label="Close Preview" id="close">
|
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
|
||||||
<i class="material-icons">close</i>
|
<i class="material-icons">close</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -11,20 +11,24 @@
|
|||||||
<info-button></info-button>
|
<info-button></info-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="action" @click="prev" v-show="hasPrevious"><i class="material-icons">chevron_left</i></button>
|
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
||||||
<button class="action" @click="next" v-show="hasNext"><i class="material-icons">chevron_right</i></button>
|
<i class="material-icons">chevron_left</i>
|
||||||
|
</button>
|
||||||
|
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
|
||||||
|
<i class="material-icons">chevron_right</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="preview">
|
<div class="preview">
|
||||||
<img v-if="req.type == 'image'" :src="raw()">
|
<img v-if="req.type == 'image'" :src="raw()">
|
||||||
<audio v-else-if="req.type == 'audio'" :src="raw()" controls></audio>
|
<audio v-else-if="req.type == 'audio'" :src="raw()" autoplay controls></audio>
|
||||||
<video v-else-if="req.type == 'video'" :src="raw()" controls>
|
<video v-else-if="req.type == 'video'" :src="raw()" autoplay controls>
|
||||||
Sorry, your browser doesn't support embedded videos,
|
Sorry, your browser doesn't support embedded videos,
|
||||||
but don't worry, you can <a :href="download()">download it</a>
|
but don't worry, you can <a :href="download()">download it</a>
|
||||||
and watch it with your favorite video player!
|
and watch it with your favorite video player!
|
||||||
</video>
|
</video>
|
||||||
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object>
|
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object>
|
||||||
<a v-else-if="req.type == 'blob'" :href="download()">
|
<a v-else-if="req.type == 'blob'" :href="download()">
|
||||||
<h2 class="message">Download <i class="material-icons">file_download</i></h2>
|
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||||
</a>
|
</a>
|
||||||
<pre v-else >{{ req.content }}</pre>
|
<pre v-else >{{ req.content }}</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,11 +38,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import InfoButton from './buttons/Info'
|
import InfoButton from '@/components/buttons/Info'
|
||||||
import DeleteButton from './buttons/Delete'
|
import DeleteButton from '@/components/buttons/Delete'
|
||||||
import RenameButton from './buttons/Rename'
|
import RenameButton from '@/components/buttons/Rename'
|
||||||
import DownloadButton from './buttons/Download'
|
import DownloadButton from '@/components/buttons/Download'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'preview',
|
name: 'preview',
|
||||||
@@ -71,7 +75,7 @@ export default {
|
|||||||
this.listing = req
|
this.listing = req
|
||||||
this.updateLinks()
|
this.updateLinks()
|
||||||
})
|
})
|
||||||
.catch(error => { console.log(error) })
|
.catch(this.$showError)
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
window.removeEventListener('keyup', this.key)
|
window.removeEventListener('keyup', this.key)
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>Copy</h3>
|
<div class="card-title">
|
||||||
<p>Choose the place to copy your files:</p>
|
<h2>{{ $t('prompts.copy') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<file-list @update:selected="val => dest = val"></file-list>
|
<div class="card-content">
|
||||||
|
<p>{{ $t('prompts.copyMessage') }}</p>
|
||||||
|
<file-list @update:selected="val => dest = val"></file-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="card-action">
|
||||||
<button class="ok" @click="copy">Copy</button>
|
<button class="cancel flat"
|
||||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button class="flat"
|
||||||
|
@click="copy"
|
||||||
|
:aria-label="$t('buttons.copy')"
|
||||||
|
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -15,7 +25,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -45,12 +55,12 @@ export default {
|
|||||||
// Execute the promises.
|
// Execute the promises.
|
||||||
api.copy(items)
|
api.copy(items)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('copy')
|
buttons.success('copy')
|
||||||
this.$router.push({ path: this.dest })
|
this.$router.push({ path: this.dest })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('copy')
|
buttons.done('copy')
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>Delete files</h3>
|
<div class="card-content">
|
||||||
<p v-show="req.kind !== 'listing'">Are you sure you want to delete this file/folder?</p>
|
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
||||||
<p v-show="req.kind === 'listing'">Are you sure you want to delete {{ selectedCount }} file(s)?</p>
|
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
||||||
<div>
|
</div>
|
||||||
<button @click="submit" autofocus>Delete</button>
|
<div class="card-action">
|
||||||
<button @click="closeHovers" class="cancel">Cancel</button>
|
<button @click="$store.commit('closeHovers')"
|
||||||
|
class="flat cancel"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button @click="submit"
|
||||||
|
class="flat"
|
||||||
|
:aria-label="$t('buttons.delete')"
|
||||||
|
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapGetters, mapMutations, mapState} from 'vuex'
|
import {mapGetters, mapMutations, mapState} from 'vuex'
|
||||||
import api from '@/utils/api'
|
import { remove } from '@/utils/api'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
@@ -31,14 +38,14 @@ export default {
|
|||||||
// If we are not on a listing, delete the current
|
// If we are not on a listing, delete the current
|
||||||
// opened file.
|
// opened file.
|
||||||
if (this.req.kind !== 'listing') {
|
if (this.req.kind !== 'listing') {
|
||||||
api.delete(this.$route.path)
|
remove(this.$route.path)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('delete')
|
buttons.success('delete')
|
||||||
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
|
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('delete')
|
buttons.done('delete')
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -54,18 +61,18 @@ export default {
|
|||||||
let promises = []
|
let promises = []
|
||||||
|
|
||||||
for (let index of this.selected) {
|
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)
|
Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('delete')
|
buttons.success('delete')
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('delete')
|
buttons.done('delete')
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt" id="download">
|
<div class="card floating" id="download">
|
||||||
<h3>Download files</h3>
|
<div class="card-title">
|
||||||
<p>Choose the format you want to download.</p>
|
<h2>{{ $t('prompts.download') }}</h2>
|
||||||
<button @click="download('zip')" autofocus>zip</button>
|
</div>
|
||||||
<button @click="download('tar')" autofocus>tar</button>
|
|
||||||
<button @click="download('targz')" autofocus>tar.gz</button>
|
<div class="card-content">
|
||||||
<button @click="download('tarbz2')" autofocus>tar.bz2</button>
|
<p>{{ $t('prompts.downloadMessage') }}</p>
|
||||||
<button @click="download('tarxz')" autofocus>tar.xz</button>
|
|
||||||
|
<button class="block cancel" @click="download('zip')" autofocus>zip</button>
|
||||||
|
<button class="block cancel" @click="download('tar')" autofocus>tar</button>
|
||||||
|
<button class="block cancel" @click="download('targz')" autofocus>tar.gz</button>
|
||||||
|
<button class="block cancel" @click="download('tarbz2')" autofocus>tar.bz2</button>
|
||||||
|
<button class="block cancel" @click="download('tarxz')" autofocus>tar.xz</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapGetters, mapState} from 'vuex'
|
import {mapGetters, mapState} from 'vuex'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'download',
|
name: 'download',
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="prompt error">
|
|
||||||
<i class="material-icons">error_outline</i>
|
|
||||||
<h3>Something went wrong</h3>
|
|
||||||
<pre>{{ $store.state.showMessage }}</pre>
|
|
||||||
<div>
|
|
||||||
<button @click="close" autofocus>Close</button>
|
|
||||||
<button @click="reportIssue" class="cancel">Report Issue</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'error',
|
|
||||||
methods: {
|
|
||||||
reportIssue () {
|
|
||||||
window.open('https://github.com/hacdias/filemanager/issues/new')
|
|
||||||
},
|
|
||||||
close () {
|
|
||||||
this.$store.commit('closeHovers')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -4,19 +4,22 @@
|
|||||||
<li @click="select"
|
<li @click="select"
|
||||||
@touchstart="touchstart"
|
@touchstart="touchstart"
|
||||||
@dblclick="next"
|
@dblclick="next"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
:aria-label="item.name"
|
||||||
:aria-selected="selected == item.url"
|
:aria-selected="selected == item.url"
|
||||||
:key="item.name" v-for="item in items"
|
:key="item.name" v-for="item in items"
|
||||||
:data-url="item.url">{{ item.name }}</li>
|
:data-url="item.url">{{ item.name }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Currently navigating on: <code>{{ nav }}</code>.</p>
|
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'file-list',
|
name: 'file-list',
|
||||||
@@ -50,7 +53,7 @@ export default {
|
|||||||
// so we fetch the data from the previous directory.
|
// so we fetch the data from the previous directory.
|
||||||
api.fetch(url.removeLastDir(this.$route.path))
|
api.fetch(url.removeLastDir(this.$route.path))
|
||||||
.then(this.fillOptions)
|
.then(this.fillOptions)
|
||||||
.catch(this.showError)
|
.catch(this.$showError)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fillOptions (req) {
|
fillOptions (req) {
|
||||||
@@ -93,7 +96,7 @@ export default {
|
|||||||
|
|
||||||
api.fetch(uri)
|
api.fetch(uri)
|
||||||
.then(this.fillOptions)
|
.then(this.fillOptions)
|
||||||
.catch(this.showError)
|
.catch(this.$showError)
|
||||||
},
|
},
|
||||||
touchstart (event) {
|
touchstart (event) {
|
||||||
let url = event.currentTarget.dataset.url
|
let url = event.currentTarget.dataset.url
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt help">
|
<div class="card floating help">
|
||||||
<h3>Help</h3>
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('help.help') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul>
|
<div class="card-content">
|
||||||
<li><strong>F1</strong> - this information</li>
|
<ul>
|
||||||
<li><strong>F2</strong> - rename file</li>
|
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
||||||
<li><strong>DEL</strong> - delete selected items</li>
|
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
||||||
<li><strong>ESC</strong> - clear selection and/or close the prompt</li>
|
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
|
||||||
<li><strong>CTRL + S</strong> - save a file or download the directory where you are</li>
|
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
|
||||||
<li><strong>CTRL + Click</strong> - select multiple files or directories</li>
|
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
|
||||||
<li><strong>Double click</strong> - open a file or directory</li>
|
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
|
||||||
<li><strong>Click</strong> - select file or directory</li>
|
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
|
||||||
</ul>
|
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
||||||
|
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>Not available yet</p>
|
<div class="card-action">
|
||||||
|
<button type="submit"
|
||||||
<ul>
|
@click="$store.commit('closeHovers')"
|
||||||
<li><strong>Alt + Click</strong> - select a group of files</li>
|
class="flat"
|
||||||
</ul>
|
:aria-label="$t('buttons.ok')"
|
||||||
|
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||||
<div>
|
|
||||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,27 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>File Information</h3>
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('prompts.fileInfo') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p v-show="selected.length > 1">{{ selected.length }} files selected.</p>
|
<div class="card-content">
|
||||||
|
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
||||||
|
|
||||||
<p v-show="selected.length < 2"><strong>Display Name:</strong> {{ name() }}</p>
|
<p v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
|
||||||
<p><strong>Size:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
||||||
<p v-show="selected.length < 2"><strong>Last Modified:</strong> {{ humanTime() }}</p>
|
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
|
||||||
|
|
||||||
<section v-show="dir() && selected.length === 0">
|
<template v-if="dir() && selected.length === 0">
|
||||||
<p><strong>Number of files:</strong> {{ req.numFiles }}</p>
|
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
|
||||||
<p><strong>Number of directories:</strong> {{ req.numDirs }}</p>
|
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
|
||||||
</section>
|
</template>
|
||||||
|
|
||||||
<section v-show="!dir()">
|
<template v-if="!dir()">
|
||||||
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">show</a></code></p>
|
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
|
||||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">show</a></code></p>
|
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
||||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">show</a></code></p>
|
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
|
||||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">show</a></code></p>
|
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
|
||||||
</section>
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="card-action">
|
||||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
|
<button type="submit"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
class="flat"
|
||||||
|
:aria-label="$t('buttons.ok')"
|
||||||
|
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -30,7 +38,7 @@
|
|||||||
import {mapState, mapGetters} from 'vuex'
|
import {mapState, mapGetters} from 'vuex'
|
||||||
import filesize from 'filesize'
|
import filesize from 'filesize'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'info',
|
name: 'info',
|
||||||
@@ -107,7 +115,7 @@ export default {
|
|||||||
|
|
||||||
api.checksum(link, hash)
|
api.checksum(link, hash)
|
||||||
.then((hash) => { event.target.innerHTML = hash })
|
.then((hash) => { event.target.innerHTML = hash })
|
||||||
.catch(error => { this.$store.commit('showError', error) })
|
.catch(this.$showError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>Move</h3>
|
<div class="card-title">
|
||||||
<p>Choose new house for your file(s)/folder(s):</p>
|
<h2>{{ $t('prompts.move') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<file-list @update:selected="val => dest = val"></file-list>
|
<div class="card-content">
|
||||||
|
<file-list @update:selected="val => dest = val"></file-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="card-action">
|
||||||
<button class="ok" @click="move">Move</button>
|
<button class="flat cancel"
|
||||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button class="flat"
|
||||||
|
@click="move"
|
||||||
|
:aria-label="$t('buttons.move')"
|
||||||
|
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -15,7 +24,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -45,12 +54,12 @@ export default {
|
|||||||
// Execute the promises.
|
// Execute the promises.
|
||||||
api.move(items)
|
api.move(items)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('move')
|
buttons.success('move')
|
||||||
this.$router.push({ path: this.dest })
|
this.$router.push({ path: this.dest })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('move')
|
buttons.done('move')
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
72
assets/src/components/prompts/NewArchetype.vue
Normal file
72
assets/src/components/prompts/NewArchetype.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{ $t('prompts.newArchetype') }}</p>
|
||||||
|
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||||
|
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="flat cancel"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button class="flat"
|
||||||
|
@click="submit"
|
||||||
|
:aria-label="$t('buttons.create')"
|
||||||
|
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { removePrefix } from '@/utils/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'new-archetype',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
archetype: 'default'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit: function (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
|
||||||
|
this.new('/' + this.name, this.archetype)
|
||||||
|
.then((url) => {
|
||||||
|
this.$router.push({ path: url })
|
||||||
|
})
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
new (url, type) {
|
||||||
|
url = removePrefix(url)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let request = new window.XMLHttpRequest()
|
||||||
|
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
|
||||||
|
if (!this.$store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
||||||
|
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
||||||
|
|
||||||
|
request.onload = () => {
|
||||||
|
if (request.status === 200) {
|
||||||
|
resolve(request.getResponseHeader('Location'))
|
||||||
|
} else {
|
||||||
|
reject(request.responseText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = (error) => reject(error)
|
||||||
|
request.send()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -1,18 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>New directory</h3>
|
<div class="card-title">
|
||||||
<p>Write the name of the new directory.</p>
|
<h2>{{ $t('prompts.newDir') }}</h2>
|
||||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
</div>
|
||||||
<div>
|
|
||||||
<button class="ok" @click="submit">Create</button>
|
<div class="card-content">
|
||||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
<p>{{ $t('prompts.newDirMessage') }}</p>
|
||||||
|
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="cancel flat"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button class="flat"
|
||||||
|
:aria-label="$t('buttons.create')"
|
||||||
|
:title="$t('buttons.create')"
|
||||||
|
@click="submit">{{ $t('buttons.create') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'new-dir',
|
name: 'new-dir',
|
||||||
@@ -37,7 +49,7 @@ export default {
|
|||||||
|
|
||||||
api.post(uri)
|
api.post(uri)
|
||||||
.then(() => { this.$router.push({ path: uri }) })
|
.then(() => { this.$router.push({ path: uri }) })
|
||||||
.catch(error => { this.$store.commit('showError', error) })
|
.catch(this.$showError)
|
||||||
|
|
||||||
// Close the prompt
|
// Close the prompt
|
||||||
this.$store.commit('closeHovers')
|
this.$store.commit('closeHovers')
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>New file</h3>
|
<div class="card-title">
|
||||||
<p>Write the name of the new file.</p>
|
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
</div>
|
||||||
<div>
|
|
||||||
<button class="ok" @click="submit">Create</button>
|
<div class="card-content">
|
||||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
<p>{{ $t('prompts.newFileMessage') }}</p>
|
||||||
|
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="cancel flat"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button class="flat"
|
||||||
|
@click="submit"
|
||||||
|
:aria-label="$t('buttons.create')"
|
||||||
|
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'new-file',
|
name: 'new-file',
|
||||||
@@ -38,7 +50,7 @@ export default {
|
|||||||
// Create the new file.
|
// Create the new file.
|
||||||
api.post(uri)
|
api.post(uri)
|
||||||
.then(() => { this.$router.push({ path: uri }) })
|
.then(() => { this.$router.push({ path: uri }) })
|
||||||
.catch(error => { this.$store.commit('showError', error) })
|
.catch(this.$showError)
|
||||||
|
|
||||||
// Close the prompt.
|
// Close the prompt.
|
||||||
this.$store.commit('closeHovers')
|
this.$store.commit('closeHovers')
|
||||||
|
|||||||
@@ -9,29 +9,10 @@
|
|||||||
<info v-else-if="showInfo"></info>
|
<info v-else-if="showInfo"></info>
|
||||||
<move v-else-if="showMove"></move>
|
<move v-else-if="showMove"></move>
|
||||||
<copy v-else-if="showCopy"></copy>
|
<copy v-else-if="showCopy"></copy>
|
||||||
<error v-else-if="showError"></error>
|
<replace v-else-if="showReplace"></replace>
|
||||||
<success v-else-if="showSuccess"></success>
|
<schedule v-else-if="show === 'schedule'"></schedule>
|
||||||
|
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
|
||||||
<template v-for="plugin in plugins">
|
<share v-else-if="show === 'share'"></share>
|
||||||
<form class="prompt"
|
|
||||||
v-for="prompt in plugin.prompts"
|
|
||||||
:key="prompt.name"
|
|
||||||
v-if="show === prompt.name"
|
|
||||||
@submit="prompt.submit($event, pluginData, $route)">
|
|
||||||
<h3>{{ prompt.title }}</h3>
|
|
||||||
<p>{{ prompt.description }}</p>
|
|
||||||
<input v-for="input in prompt.inputs"
|
|
||||||
:key="input.name"
|
|
||||||
:type="input.type"
|
|
||||||
:name="input.name"
|
|
||||||
:placeholder="input.placeholder">
|
|
||||||
<div>
|
|
||||||
<input type="submit" class="ok" :value="prompt.ok">
|
|
||||||
<button class="cancel" @click.prevent="$store.commit('closeHovers')">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -44,28 +25,32 @@ import Rename from './Rename'
|
|||||||
import Download from './Download'
|
import Download from './Download'
|
||||||
import Move from './Move'
|
import Move from './Move'
|
||||||
import Copy from './Copy'
|
import Copy from './Copy'
|
||||||
import Error from './Error'
|
|
||||||
import Success from './Success'
|
|
||||||
import NewFile from './NewFile'
|
import NewFile from './NewFile'
|
||||||
import NewDir from './NewDir'
|
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 { mapState } from 'vuex'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'prompts',
|
name: 'prompts',
|
||||||
components: {
|
components: {
|
||||||
Info,
|
Info,
|
||||||
Delete,
|
Delete,
|
||||||
|
NewArchetype,
|
||||||
|
Schedule,
|
||||||
Rename,
|
Rename,
|
||||||
Error,
|
|
||||||
Download,
|
Download,
|
||||||
Success,
|
|
||||||
Move,
|
Move,
|
||||||
Copy,
|
Copy,
|
||||||
|
Share,
|
||||||
NewFile,
|
NewFile,
|
||||||
NewDir,
|
NewDir,
|
||||||
Help
|
Help,
|
||||||
|
Replace
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
@@ -79,8 +64,6 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['show', 'plugins']),
|
...mapState(['show', 'plugins']),
|
||||||
showError: function () { return this.show === 'error' },
|
|
||||||
showSuccess: function () { return this.show === 'success' },
|
|
||||||
showInfo: function () { return this.show === 'info' },
|
showInfo: function () { return this.show === 'info' },
|
||||||
showHelp: function () { return this.show === 'help' },
|
showHelp: function () { return this.show === 'help' },
|
||||||
showDelete: function () { return this.show === 'delete' },
|
showDelete: function () { return this.show === 'delete' },
|
||||||
@@ -90,6 +73,7 @@ export default {
|
|||||||
showNewFile: function () { return this.show === 'newFile' },
|
showNewFile: function () { return this.show === 'newFile' },
|
||||||
showNewDir: function () { return this.show === 'newDir' },
|
showNewDir: function () { return this.show === 'newDir' },
|
||||||
showDownload: function () { return this.show === 'download' },
|
showDownload: function () { return this.show === 'download' },
|
||||||
|
showReplace: function () { return this.show === 'replace' },
|
||||||
showOverlay: function () {
|
showOverlay: function () {
|
||||||
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
|
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>Rename</h3>
|
<div class="card-title">
|
||||||
<p>Insert a new name for <code>{{ oldName() }}</code>:</p>
|
<h2>{{ $t('prompts.rename') }}</h2>
|
||||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
</div>
|
||||||
<div>
|
|
||||||
<button @click="submit" type="submit">Rename</button>
|
<div class="card-content">
|
||||||
<button @click="cancel" class="cancel">Cancel</button>
|
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
||||||
|
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="cancel flat"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button @click="submit"
|
||||||
|
class="flat"
|
||||||
|
type="submit"
|
||||||
|
:aria-label="$t('buttons.rename')"
|
||||||
|
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -13,7 +26,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'rename',
|
name: 'rename',
|
||||||
@@ -61,7 +74,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$store.commit('closeHovers')
|
this.$store.commit('closeHovers')
|
||||||
|
|||||||
31
assets/src/components/prompts/Replace.vue
Normal file
31
assets/src/components/prompts/Replace.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('prompts.replace') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="flat cancel"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button class="flat"
|
||||||
|
@click="showConfirm"
|
||||||
|
:aria-label="$t('buttons.replace')"
|
||||||
|
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'replace',
|
||||||
|
computed: mapState(['showConfirm'])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
47
assets/src/components/prompts/Schedule.vue
Normal file
47
assets/src/components/prompts/Schedule.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('prompts.schedule') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{ $t('prompts.scheduleMessage') }}</p>
|
||||||
|
<input autofocus type="datetime-local" v-model="date">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="cancel flat"
|
||||||
|
@click="close"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button class="falt"
|
||||||
|
@click="submit"
|
||||||
|
:aria-label="$t('buttons.schedule')"
|
||||||
|
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'schedule',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
date: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close () {
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
},
|
||||||
|
submit: function (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (this.date === '') return
|
||||||
|
this.close()
|
||||||
|
this.$store.commit('setSchedule', this.date)
|
||||||
|
document.getElementById('save-button').click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
162
assets/src/components/prompts/Share.vue
Normal file
162
assets/src/components/prompts/Share.vue
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating" id="share">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('buttons.share') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<ul>
|
||||||
|
<li v-if="!hasPermanent">
|
||||||
|
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-for="link in links" :key="link.hash">
|
||||||
|
<a :href="buildLink(link.hash)" target="_blank">
|
||||||
|
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
|
||||||
|
<template v-else>{{ $t('permanent') }}</template>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="action"
|
||||||
|
@click="deleteLink($event, link)"
|
||||||
|
:aria-label="$t('buttons.delete')"
|
||||||
|
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||||
|
|
||||||
|
<button class="action copy-clipboard"
|
||||||
|
:data-clipboard-text="buildLink(link.hash)"
|
||||||
|
:aria-label="$t('buttons.copyToClipboard')"
|
||||||
|
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<input autofocus
|
||||||
|
type="number"
|
||||||
|
max="2147483647"
|
||||||
|
min="0"
|
||||||
|
@keyup.enter="submit"
|
||||||
|
v-model.trim="time">
|
||||||
|
<select v-model="unit" :aria-label="$t('time.unit')">
|
||||||
|
<option value="seconds">{{ $t('time.seconds') }}</option>
|
||||||
|
<option value="minutes">{{ $t('time.minutes') }}</option>
|
||||||
|
<option value="hours">{{ $t('time.hours') }}</option>
|
||||||
|
<option value="days">{{ $t('time.days') }}</option>
|
||||||
|
</select>
|
||||||
|
<button class="action"
|
||||||
|
@click="submit"
|
||||||
|
:aria-label="$t('buttons.create')"
|
||||||
|
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="flat"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.close')"
|
||||||
|
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import { getShare, deleteShare, share } from '@/utils/api'
|
||||||
|
import moment from 'moment'
|
||||||
|
import Clipboard from 'clipboard'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'share',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
time: '',
|
||||||
|
unit: 'hours',
|
||||||
|
hasPermanent: false,
|
||||||
|
links: [],
|
||||||
|
clip: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState([ 'baseURL', 'req', 'selected', 'selectedCount' ]),
|
||||||
|
url () {
|
||||||
|
// Get the current name of the file we are editing.
|
||||||
|
if (this.req.kind !== 'listing') {
|
||||||
|
return this.$route.path
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||||
|
// This shouldn't happen.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.req.items[this.selected[0]].url
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeMount () {
|
||||||
|
getShare(this.url)
|
||||||
|
.then(links => {
|
||||||
|
this.links = links
|
||||||
|
this.sort()
|
||||||
|
|
||||||
|
for (let link of this.links) {
|
||||||
|
if (!link.expires) {
|
||||||
|
this.hasPermanent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error === 404) return
|
||||||
|
this.$showError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.clip = new Clipboard('.copy-clipboard')
|
||||||
|
this.clip.on('success', (e) => {
|
||||||
|
this.$showSuccess(this.$t('success.linkCopied'))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
this.clip.destroy()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit: function (event) {
|
||||||
|
if (!this.time) return
|
||||||
|
|
||||||
|
share(this.url, this.time, this.unit)
|
||||||
|
.then(result => { this.links.push(result); this.sort() })
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
getPermalink (event) {
|
||||||
|
share(this.url)
|
||||||
|
.then(result => {
|
||||||
|
this.links.push(result)
|
||||||
|
this.sort()
|
||||||
|
this.hasPermanent = true
|
||||||
|
})
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
deleteLink (event, link) {
|
||||||
|
event.preventDefault()
|
||||||
|
deleteShare(link.hash)
|
||||||
|
.then(() => {
|
||||||
|
if (!link.expires) this.hasPermanent = false
|
||||||
|
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||||
|
})
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
humanTime (time) {
|
||||||
|
return moment(time).fromNow()
|
||||||
|
},
|
||||||
|
buildLink (hash) {
|
||||||
|
return `${window.location.origin}${this.baseURL}/share/${hash}`
|
||||||
|
},
|
||||||
|
sort () {
|
||||||
|
this.links = this.links.sort((a, b) => {
|
||||||
|
if (!a.expires) return -1
|
||||||
|
if (!b.expires) return 1
|
||||||
|
return new Date(a.expireDate) - new Date(b.expireDate)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="prompt success">
|
|
||||||
<i class="material-icons">done</i>
|
|
||||||
<h3>{{ $store.state.showMessage }}</h3>
|
|
||||||
<div>
|
|
||||||
<button @click="close" autofocus>OK</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'success',
|
|
||||||
methods: {
|
|
||||||
close () {
|
|
||||||
this.$store.commit('closeHovers')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Roboto', sans-serif;
|
||||||
padding-top: 4em;
|
padding-top: 4em;
|
||||||
background-color: #f8f8f8;
|
background-color: #fafafa;
|
||||||
user-select: none;
|
color: #333333;
|
||||||
color: #212121;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -66,6 +65,53 @@ button:hover {
|
|||||||
background-color: #1E88E5;
|
background-color: #1E88E5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="submit"].block,
|
||||||
|
button.block {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.delete {
|
||||||
|
background: #F44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.delete:hover {
|
||||||
|
background: #D32F2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.cancel {
|
||||||
|
background-color: #ECEFF1;
|
||||||
|
color: #37474F;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.cancel:hover {
|
||||||
|
background-color: #e9eaeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.flat,
|
||||||
|
input[type="submit"].flat {
|
||||||
|
color: #1E88E5;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: 0 0 0;
|
||||||
|
border: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.flat:hover,
|
||||||
|
input[type="submit"].flat:hover {
|
||||||
|
background: rgba(0,0,0,0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
button.flat.delete {
|
||||||
|
color: #F44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.flat.cancel {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-only {
|
.mobile-only {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -145,3 +191,19 @@ main {
|
|||||||
#breadcrumbs span a {
|
#breadcrumbs span a {
|
||||||
padding: .2em;
|
padding: .2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#progress {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
z-index: 9999999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress div {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #40c4ff;
|
||||||
|
width: 0;
|
||||||
|
transition: .2s ease width;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,110 +1,70 @@
|
|||||||
.dashboard {
|
.dashboard {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
|
||||||
border-radius: .5em;
|
|
||||||
background: #fff;
|
|
||||||
padding: 1em;
|
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard a {
|
a {
|
||||||
color: inherit
|
color: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard h1 button {
|
select,
|
||||||
font-size: 0.5em;
|
textarea,
|
||||||
float: right;
|
input[type="text"],
|
||||||
}
|
input[type="password"] {
|
||||||
|
padding: 0.5em 0;
|
||||||
.dashboard table {
|
line-height: 1;
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard table th {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #757575;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard table th,
|
|
||||||
.dashboard table td {
|
|
||||||
padding: .5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard table td:last-child {
|
|
||||||
width: 1em
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard > *:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard form > p:last-child,
|
|
||||||
form.dashboard > p:last-child {
|
|
||||||
text-align: right
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard > *:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard textarea,
|
|
||||||
.dashboard input[type="text"],
|
|
||||||
.dashboard input[type="password"] {
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1.7;
|
|
||||||
display: block;
|
display: block;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px solid #dddddd;
|
border-bottom: 1px solid #dddddd;
|
||||||
transition: .2s ease border;
|
transition: .2s ease border;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
line-height: 1.15;
|
||||||
|
padding: .5em;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
font-family: monospace;
|
||||||
|
min-height: 10em;
|
||||||
|
resize: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard #locale,
|
||||||
.dashboard #username,
|
.dashboard #username,
|
||||||
.dashboard #password,
|
.dashboard #password,
|
||||||
.dashboard #scope {
|
.dashboard #scope {
|
||||||
max-width: 18em;
|
max-width: 18em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard textarea:focus,
|
.dashboard #locale {
|
||||||
.dashboard textarea:hover,
|
margin-top: .5em;
|
||||||
.dashboard input[type="text"]:focus,
|
}
|
||||||
.dashboard input[type="password"]:focus,
|
|
||||||
.dashboard input[type="text"]:hover,
|
textarea:focus,
|
||||||
.dashboard input[type="password"]:hover {
|
textarea:hover,
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="password"]:focus,
|
||||||
|
input[type="text"]:hover,
|
||||||
|
input[type="password"]:hover {
|
||||||
border-color: #2979ff;
|
border-color: #2979ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard input.red {
|
input.red {
|
||||||
border-color: red;
|
border-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard input.green {
|
input.green {
|
||||||
border-color: green;
|
border-color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard button.delete {
|
|
||||||
background: #F44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard button.delete:hover {
|
|
||||||
background: #D32F2F;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard textarea {
|
|
||||||
line-height: 1.15;
|
|
||||||
padding: .5em;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
font-family: monospace;
|
|
||||||
min-height: 10em;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard p label {
|
.dashboard p label {
|
||||||
margin-bottom: .2em;
|
margin-bottom: .2em;
|
||||||
display: block;
|
display: block;
|
||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
|
color: rgba(0, 0, 0, 0.57);
|
||||||
}
|
}
|
||||||
|
|
||||||
li code,
|
li code,
|
||||||
@@ -118,3 +78,343 @@ p code {
|
|||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard #nav {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
color: rgb(84, 110, 122);
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 0 1em;
|
||||||
|
font-size: .8em;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard #nav li {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 0 1em;
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard #nav li.active {
|
||||||
|
border-color: #2196f3
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard #nav i {
|
||||||
|
font-size: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr {
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:last-child {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #757575;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th,
|
||||||
|
table td {
|
||||||
|
padding: .5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.small {
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr>*:first-child {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr>*:last-child {
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
margin: .5rem 0 1rem 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.floating {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 99999;
|
||||||
|
max-width: 25em;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 95%;
|
||||||
|
z-index: 99999;
|
||||||
|
animation: .1s show forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card>*>*:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card>*>*:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-title {
|
||||||
|
padding: 1.5em 1em 1em;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-title>*:first-child {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card>div {
|
||||||
|
padding: 1em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card>div:first-child {
|
||||||
|
padding-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card>div:last-child {
|
||||||
|
padding-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-title * {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-action {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-content.full {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
color: rgba(0, 0, 0, 0.53);
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 2em 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content table {
|
||||||
|
margin: 0 -1em;
|
||||||
|
width: calc(100% + 2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card code {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card#download {
|
||||||
|
max-width: 15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card#share ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card#share ul li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card#share ul li a {
|
||||||
|
color: #2196F3;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card#share ul li .action i {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card#share ul li input,
|
||||||
|
.card#share ul li select {
|
||||||
|
padding: .2em;
|
||||||
|
margin-right: .5em;
|
||||||
|
border: 1px solid #dadada;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card#share .action.copy-clipboard::after {
|
||||||
|
content: 'Copied!';
|
||||||
|
position: absolute;
|
||||||
|
left: -25%;
|
||||||
|
width: 150%;
|
||||||
|
font-size: .6em;
|
||||||
|
text-align: center;
|
||||||
|
background: #44a6f5;
|
||||||
|
color: #fff;
|
||||||
|
padding: .5em .2em;
|
||||||
|
border-radius: .4em;
|
||||||
|
top: -2em;
|
||||||
|
transition: .1s ease opacity;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card#share .action.copy-clipboard.active::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 9999;
|
||||||
|
animation: .1s show forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* * * * * * * * * * * * * * * *
|
||||||
|
* PROMPT - MOVE *
|
||||||
|
* * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow: auto;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list li {
|
||||||
|
width: 100%;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: .2em;
|
||||||
|
padding: .3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list li[aria-selected=true] {
|
||||||
|
background: #2196f3 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
transition: .1s ease all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list li:hover {
|
||||||
|
background-color: #e9eaeb;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list li:before {
|
||||||
|
content: "folder";
|
||||||
|
color: #6f6f6f;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: 'Material Icons';
|
||||||
|
font-size: 1.75em;
|
||||||
|
margin-right: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list li[aria-selected=true]:before {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help {
|
||||||
|
max-width: 24em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help ul {
|
||||||
|
padding: 0;
|
||||||
|
margin: 1em 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes show {
|
||||||
|
0% {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
1% {
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible:last-of-type {
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible > input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible > label {
|
||||||
|
padding: 1em 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border-right: 0;
|
||||||
|
border-left: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible > label * {
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(0,0,0,0.57);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible > label i {
|
||||||
|
transition: .2s ease transform;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible .collapse {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: .2s ease all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible > input:checked ~ .collapse {
|
||||||
|
padding-top: 1em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
max-height: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible > input:checked ~ label i {
|
||||||
|
transform: rotate(180deg)
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .collapsible {
|
||||||
|
width: calc(100% + 2em);
|
||||||
|
margin: 0 -1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .collapsible > label {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .collapsible .collapse {
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ header>div div {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
header > div:last-child div {
|
header>div:last-child div {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,17 +122,18 @@ header .search-button {
|
|||||||
#search input {
|
#search input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: 0;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
line-height: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search #result {
|
#search #result {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
background-color: #fff;
|
background-color: #f8f8f8;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: #ccc;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
color: rgba(0, 0, 0, 0.6);
|
||||||
height: 0;
|
height: 0;
|
||||||
transition: .1s ease height, .1s ease padding;
|
transition: .1s ease height, .1s ease padding;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@@ -140,6 +141,10 @@ header .search-button {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#search #result>div>*:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#search.active #result {
|
#search.active #result {
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
height: calc(100% - 4em);
|
height: calc(100% - 4em);
|
||||||
@@ -155,15 +160,12 @@ header .search-button {
|
|||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search #result div {
|
#search #result>div {
|
||||||
white-space: pre-wrap;
|
max-width: 45em;
|
||||||
white-space: -moz-pre-wrap;
|
margin: 0 auto;
|
||||||
white-space: -pre-wrap;
|
|
||||||
white-space: -o-pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#search #result p {
|
#search #result #renew {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -171,17 +173,30 @@ header .search-button {
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search.ongoing #result p {
|
#search.ongoing #result #renew {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search.active #result i {
|
#search.active #result i {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search.active #result>p>i {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#search.active #result ul li a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: .3em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search.active #result ul li a i {
|
||||||
|
margin-right: .3em;
|
||||||
|
}
|
||||||
|
|
||||||
#search::-webkit-input-placeholder {
|
#search::-webkit-input-placeholder {
|
||||||
color: rgba(255, 255, 255, .5);
|
color: rgba(255, 255, 255, .5);
|
||||||
}
|
}
|
||||||
@@ -199,3 +214,47 @@ header .search-button {
|
|||||||
#search:-ms-input-placeholder {
|
#search:-ms-input-placeholder {
|
||||||
color: rgba(255, 255, 255, .5);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
|
|
||||||
#listing.list .item.header {
|
#listing.list .item.header {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
background: #f8f8f8;
|
background: #fafafa;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: calc(100% - 19em);
|
width: calc(100% - 19em);
|
||||||
top: 7em;
|
top: 7em;
|
||||||
|
|||||||
68
assets/src/css/login.css
Normal file
68
assets/src/css/login.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#login {
|
||||||
|
background: #fff;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login img {
|
||||||
|
width: 4em;
|
||||||
|
height: 4em;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin: .4em 0 .67em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login form {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
max-width: 16em;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login input {
|
||||||
|
width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: .5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login .wrong {
|
||||||
|
background: #F44336;
|
||||||
|
color: #fff;
|
||||||
|
padding: .5em;
|
||||||
|
text-align: center;
|
||||||
|
animation: .2s opac forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes opac {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#login input[type="text"],
|
||||||
|
#login input[type="password"] {
|
||||||
|
padding: .5em 1em;
|
||||||
|
border: 1px solid #e9e9e9;
|
||||||
|
transition: .2s ease border;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login input[type="text"]:focus,
|
||||||
|
#login input[type="password"]:focus,
|
||||||
|
#login input[type="text"]:hover,
|
||||||
|
#login input[type="password"]:hover {
|
||||||
|
border-color: #9f9f9f;
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
max-width: 18em;
|
max-width: 20em;
|
||||||
}
|
}
|
||||||
#file-selection .action {
|
#file-selection .action {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
.prompt {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.075);
|
|
||||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 2em;
|
|
||||||
max-width: 25em;
|
|
||||||
width: 90%;
|
|
||||||
max-height: 95%;
|
|
||||||
z-index: 99999;
|
|
||||||
animation: .1s show forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 9999;
|
|
||||||
animation: .1s show forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt p {
|
|
||||||
font-size: .9em;
|
|
||||||
color: rgba(0, 0, 0, 0.8);
|
|
||||||
margin: .5em 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt input:not([type="submit"]) {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #dadada;
|
|
||||||
line-height: 1;
|
|
||||||
padding: .3em;
|
|
||||||
margin: .3em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt code {
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt div:last-of-type {
|
|
||||||
margin-top: 1em;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt .cancel {
|
|
||||||
background-color: #ECEFF1;
|
|
||||||
color: #37474F;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt .cancel:hover {
|
|
||||||
background-color: #e9eaeb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt.success i,
|
|
||||||
.prompt.error i {
|
|
||||||
color: #F44336;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto .15em;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt.success h3,
|
|
||||||
.prompt.error h3 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt.error button:not(.cancel) {
|
|
||||||
background-color: #F44336
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt.success i {
|
|
||||||
color: #8BC34A;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt.success button {
|
|
||||||
background-color: #8BC34A;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * *
|
|
||||||
* PROMPT - MOVE *
|
|
||||||
* * * * * * * * * * * * * * * */
|
|
||||||
|
|
||||||
.file-list {
|
|
||||||
max-height: 50vh;
|
|
||||||
overflow: auto;
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list li {
|
|
||||||
width: 100%;
|
|
||||||
user-select: none;
|
|
||||||
border-radius: .2em;
|
|
||||||
padding: .3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list li[aria-selected=true] {
|
|
||||||
background: #2196f3 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
transition: .1s ease all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list li:hover {
|
|
||||||
background-color: #e9eaeb;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list li:before {
|
|
||||||
content: "folder";
|
|
||||||
color: #6f6f6f;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 1.4;
|
|
||||||
font-family: 'Material Icons';
|
|
||||||
font-size: 1.75em;
|
|
||||||
margin-right: .25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list li[aria-selected=true]:before {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt#download {
|
|
||||||
max-width: 15em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt#download button {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
margin: 0 0 1em;
|
|
||||||
background-color: #ECEFF1;
|
|
||||||
color: #37474F;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt#download button:last-of-type {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help {
|
|
||||||
max-width: 24em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help ul {
|
|
||||||
padding: 0;
|
|
||||||
margin: 1em 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes show {
|
|
||||||
0% {
|
|
||||||
display: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
1% {
|
|
||||||
display: block;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
display: block;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
@import "~normalize.css/normalize.css";
|
@import "~normalize.css/normalize.css";
|
||||||
|
@import "~noty/lib/noty.css";
|
||||||
@import "./fonts.css";
|
@import "./fonts.css";
|
||||||
@import "./base.css";
|
@import "./base.css";
|
||||||
@import "./header.css";
|
@import "./header.css";
|
||||||
@import "./prompts.css";
|
|
||||||
@import "./listing.css";
|
@import "./listing.css";
|
||||||
@import "./editor.css";
|
@import "./editor.css";
|
||||||
@import "./dashboard.css";
|
@import "./dashboard.css";
|
||||||
|
@import "./login.css";
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * *
|
/* * * * * * * * * * * * * * * *
|
||||||
* ACTION *
|
* ACTION *
|
||||||
@@ -179,6 +180,17 @@
|
|||||||
* PROMPT *
|
* PROMPT *
|
||||||
* * * * * * * * * * * * * * * */
|
* * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
.noty_buttons {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 10px 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noty_buttons button {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid rgba(0,0,0,0.1);
|
||||||
|
box-shadow: 0 0 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * *
|
/* * * * * * * * * * * * * * * *
|
||||||
* FOOTER *
|
* FOOTER *
|
||||||
|
|||||||
200
assets/src/i18n/en.yaml
Normal file
200
assets/src/i18n/en.yaml
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
permanent: Permanent
|
||||||
|
buttons:
|
||||||
|
cancel: Cancel
|
||||||
|
close: Close
|
||||||
|
copy: Copy
|
||||||
|
copyFile: Copy file
|
||||||
|
copyToClipboard: Copy to clipboard
|
||||||
|
create: Create
|
||||||
|
delete: Delete
|
||||||
|
download: Download
|
||||||
|
info: Info
|
||||||
|
more: More
|
||||||
|
move: Move
|
||||||
|
moveFile: Move file
|
||||||
|
new: New
|
||||||
|
next: Next
|
||||||
|
ok: OK
|
||||||
|
replace: Replace
|
||||||
|
previous: Previous
|
||||||
|
rename: Rename
|
||||||
|
reportIssue: Report Issue
|
||||||
|
save: Save
|
||||||
|
search: Search
|
||||||
|
select: Select
|
||||||
|
share: Share
|
||||||
|
publish: Publish
|
||||||
|
selectMultiple: Select multiple
|
||||||
|
schedule: Schedule
|
||||||
|
switchView: Switch view
|
||||||
|
toggleSidebar: Toggle sidebar
|
||||||
|
update: Update
|
||||||
|
upload: Upload
|
||||||
|
permalink: Get Permanent Link
|
||||||
|
success:
|
||||||
|
linkCopied: Link copied!
|
||||||
|
errors:
|
||||||
|
forbidden: You're not welcome here.
|
||||||
|
internal: Something really went wrong.
|
||||||
|
notFound: This location can't be reached.
|
||||||
|
files:
|
||||||
|
folders: Folders
|
||||||
|
files: Files
|
||||||
|
body: Body
|
||||||
|
clear: Clear
|
||||||
|
closePreview: Close preview
|
||||||
|
home: Home
|
||||||
|
lastModified: Last modified
|
||||||
|
loading: Loading...
|
||||||
|
lonely: It feels lonely here...
|
||||||
|
metadata: Metadata
|
||||||
|
multipleSelectionEnabled: Multiple selection enabled
|
||||||
|
name: Name
|
||||||
|
size: Size
|
||||||
|
sortByName: Sort by name
|
||||||
|
sortBySize: Sort by size
|
||||||
|
sortByLastModified: Sort by last modified
|
||||||
|
help:
|
||||||
|
click: select file or directory
|
||||||
|
ctrl:
|
||||||
|
click: select multiple files or directories
|
||||||
|
f: opens search
|
||||||
|
s: save a file or download the directory where you are
|
||||||
|
del: delete selected items
|
||||||
|
doubleClick: open a file or directory
|
||||||
|
esc: clear selection and/or close the prompt
|
||||||
|
f1: this information
|
||||||
|
f2: rename file
|
||||||
|
help: Help
|
||||||
|
login:
|
||||||
|
password: Password
|
||||||
|
submit: Login
|
||||||
|
username: Username
|
||||||
|
wrongCredentials: Wrong credentials
|
||||||
|
prompts:
|
||||||
|
copy: Copy
|
||||||
|
copyMessage: 'Choose the place to copy your files:'
|
||||||
|
currentlyNavigating: 'Currently navigating on:'
|
||||||
|
deleteMessageMultiple: Are you sure you want to delete {count} file(s)?
|
||||||
|
deleteMessageSingle: Are you sure you want to delete this file/folder?
|
||||||
|
deleteTitle: Delete files
|
||||||
|
displayName: 'Display Name:'
|
||||||
|
download: Download files
|
||||||
|
downloadMessage: Choose the format you want to download.
|
||||||
|
error: Something went wrong
|
||||||
|
fileInfo: File information
|
||||||
|
filesSelected: "{count} files selected."
|
||||||
|
lastModified: Last Modified
|
||||||
|
move: Move
|
||||||
|
moveMessage: 'Choose new house for your file(s)/folder(s):'
|
||||||
|
newDir: New directory
|
||||||
|
newDirMessage: Write the name of the new directory.
|
||||||
|
newFile: New file
|
||||||
|
newFileMessage: Write the name of the new file.
|
||||||
|
numberDirs: Number of directories
|
||||||
|
numberFiles: Number of files
|
||||||
|
replace: Replace
|
||||||
|
replaceMessage: >
|
||||||
|
One of the files you're trying to upload is conflicting because of its name.
|
||||||
|
Do you wish to replace the existing one?
|
||||||
|
rename: Rename
|
||||||
|
renameMessage: Insert a new name for
|
||||||
|
show: Show
|
||||||
|
size: Size
|
||||||
|
schedule: Schedule
|
||||||
|
scheduleMessage: Pick a date and time to schedule the publication of this post.
|
||||||
|
newArchetype: Create a new post based on an archetype. Your file will be created on content folder.
|
||||||
|
settings:
|
||||||
|
admin: Admin
|
||||||
|
administrator: Administrator
|
||||||
|
allowCommands: Execute commands
|
||||||
|
allowEdit: Edit, rename and delete files or directories
|
||||||
|
allowNew: Create new files and directories
|
||||||
|
allowPublish: Publish new posts and pages
|
||||||
|
avoidChanges: "(leave blank to avoid changes)"
|
||||||
|
changePassword: Change Password
|
||||||
|
commands: Commands
|
||||||
|
commandsHelp: >
|
||||||
|
Here you can set commands that are executed in the named events. You
|
||||||
|
write one command per line. If the event is related to files, such as before and
|
||||||
|
after saving, the environment variable "FILE" will be available with the path
|
||||||
|
of the file.
|
||||||
|
commandsUpdated: Commands updated!
|
||||||
|
customStylesheet: Custom Stylesheet
|
||||||
|
examples: Examples
|
||||||
|
globalSettings: Global Settings
|
||||||
|
language: Language
|
||||||
|
lockPassword: Prevent the user from changing the password
|
||||||
|
newPassword: Your new password
|
||||||
|
newPasswordConfirm: Confirm your new password
|
||||||
|
newUser: New User
|
||||||
|
password: Password
|
||||||
|
passwordUpdated: Password updated!
|
||||||
|
permissions: Permissions
|
||||||
|
permissionsHelp: >
|
||||||
|
You can set the user to be an administrator or choose the permissions
|
||||||
|
individually. If you select "Administrator", all of the other options will be
|
||||||
|
automatically checked. The management of users remains a privilege of an administrator.
|
||||||
|
profileSettings: Profile Settings
|
||||||
|
ruleExample1: >
|
||||||
|
prevents the access to any dot file (such as .git, .gitignore) in
|
||||||
|
every folder.
|
||||||
|
ruleExample2: blocks the access to the file named Caddyfile on the root of the scope.
|
||||||
|
rules: Rules
|
||||||
|
rulesHelp1: >
|
||||||
|
Here you can define a set of allow and disallow rules for this specific
|
||||||
|
user. The blocked files won't show up in the listings and they wont be accessible
|
||||||
|
to the user. We support regex and paths relative to the users scope.
|
||||||
|
rulesHelp2: >
|
||||||
|
Each rule goes in one different line and must start with the keyword
|
||||||
|
{0} or {1}. Then you should write {2} if you are using a regular expression and
|
||||||
|
then the expression or the path.
|
||||||
|
scope: Scope
|
||||||
|
settingsUpdated: Settings updated!
|
||||||
|
user: User
|
||||||
|
userCommands: Commands
|
||||||
|
userCommandsHelp: >
|
||||||
|
A space separated list with the available commands for this user.
|
||||||
|
Example:
|
||||||
|
userCreated: User created!
|
||||||
|
userDeleted: User deleted!
|
||||||
|
userManagement: User Management
|
||||||
|
username: Username
|
||||||
|
users: Users
|
||||||
|
userUpdated: User updated!
|
||||||
|
sidebar:
|
||||||
|
help: Help
|
||||||
|
logout: Logout
|
||||||
|
myFiles: My files
|
||||||
|
newFile: New file
|
||||||
|
newFolder: New folder
|
||||||
|
settings: Settings
|
||||||
|
siteSettings: Site Settings
|
||||||
|
hugoNew: Hugo New
|
||||||
|
preview: Preview
|
||||||
|
search:
|
||||||
|
images: Images
|
||||||
|
music: Music
|
||||||
|
pdf: PDF
|
||||||
|
pressToExecute: Press enter to execute.
|
||||||
|
pressToSearch: Press enter to search.
|
||||||
|
search: Search...
|
||||||
|
searchOrCommand: Search or execute a command...
|
||||||
|
searchOrSupportedCommand: 'Search or use one of your supported commands:'
|
||||||
|
type: Type and press enter to search.
|
||||||
|
types: Types
|
||||||
|
video: Video
|
||||||
|
writeToSearch: Write here to search
|
||||||
|
languages:
|
||||||
|
en: English
|
||||||
|
fr: Français
|
||||||
|
pt: Português
|
||||||
|
ja: 日本語
|
||||||
|
zhCN: 中文 (简体)
|
||||||
|
zhTW: 中文 (繁體)
|
||||||
|
time:
|
||||||
|
unit: Time Unit
|
||||||
|
seconds: Seconds
|
||||||
|
minutes: Minutes
|
||||||
|
hours: Hours
|
||||||
|
days: Days
|
||||||
193
assets/src/i18n/fr.yaml
Normal file
193
assets/src/i18n/fr.yaml
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
permanent: Permanent
|
||||||
|
buttons:
|
||||||
|
cancel: Annuler
|
||||||
|
close: Fermer
|
||||||
|
copy: Copier
|
||||||
|
copyFile: Copier le fichier
|
||||||
|
copyToClipboard: Copier dans le presse-papier
|
||||||
|
create: Créer
|
||||||
|
delete: Supprimer
|
||||||
|
download: Télécharger
|
||||||
|
info: Info
|
||||||
|
more: Plus
|
||||||
|
move: Déplacer
|
||||||
|
moveFile: Déplacer le fichier
|
||||||
|
new: Nouveau
|
||||||
|
next: Suivant
|
||||||
|
ok: OK
|
||||||
|
replace: Remplacer
|
||||||
|
previous: Précédent
|
||||||
|
rename: Renommer
|
||||||
|
reportIssue: Rapport d'erreur
|
||||||
|
save: Enregistrer
|
||||||
|
search: Chercher
|
||||||
|
select: Sélectionner
|
||||||
|
share: Partager
|
||||||
|
publish: Publier
|
||||||
|
selectMultiple: Sélection multiple
|
||||||
|
schedule: Fixer la date
|
||||||
|
switchView: Changer le mode d'affichage
|
||||||
|
toggleSidebar: Afficher/Masquer la barre latérale
|
||||||
|
update: Mettre à jour
|
||||||
|
upload: Importer
|
||||||
|
permalink: Obtenir un lien permanent
|
||||||
|
errors:
|
||||||
|
forbidden: Vous n'êtes pas autorisé à être ici.
|
||||||
|
internal: Aïe ! Quelque chose s'est mal passé.
|
||||||
|
notFound: Impossible d'accéder à cet emplacement.
|
||||||
|
files:
|
||||||
|
folders: Dossiers
|
||||||
|
files: Fichiers
|
||||||
|
body: Corps
|
||||||
|
clear: Fermer
|
||||||
|
closePreview: Fermer la prévisualisation
|
||||||
|
home: Accueil
|
||||||
|
lastModified: Dernière modification
|
||||||
|
loading: Chargement...
|
||||||
|
lonely: Il semble qu'il n'y ai rien par ici...
|
||||||
|
metadata: Metadonnées
|
||||||
|
multipleSelectionEnabled: Sélection multiple activée
|
||||||
|
name: Nom
|
||||||
|
size: Taille
|
||||||
|
sortByName: Trier par nom
|
||||||
|
sortBySize: Trier par taille
|
||||||
|
sortByLastModified: Trier par date de dernière modification
|
||||||
|
help:
|
||||||
|
click: Sélectionner un élément
|
||||||
|
ctrl:
|
||||||
|
click: Sélectionner plusieurs éléments
|
||||||
|
f: Ouvrir l'invité de recherche
|
||||||
|
s: Télécharger l'élément actuel
|
||||||
|
del: Supprimer les éléments sélectionnés
|
||||||
|
doubleClick: Ouvrir un élément
|
||||||
|
esc: Désélectionner et/ou fermer la boîte de dialogue
|
||||||
|
f1: Ouvrir l'aide
|
||||||
|
f2: Renommer le fichier
|
||||||
|
help: Aide
|
||||||
|
login:
|
||||||
|
password: Mot de passe
|
||||||
|
submit: Se connecter
|
||||||
|
username: Utilisateur
|
||||||
|
wrongCredentials: Identifiants incorrects !
|
||||||
|
prompts:
|
||||||
|
copy: Copier
|
||||||
|
copyMessage: 'Choisissez l''emplacement où copier la sélection :'
|
||||||
|
currentlyNavigating: 'Dossier courant :'
|
||||||
|
deleteMessageMultiple: Etes-vous sûr de vouloir supprimer ces {count} élément(s) ?
|
||||||
|
deleteMessageSingle: Etes-vous sûr de vouloir supprimer cet élément ?
|
||||||
|
deleteTitle: Supprimer
|
||||||
|
displayName: 'Nom :'
|
||||||
|
download: Télécharger
|
||||||
|
downloadMessage: 'Choisissez le format de téléchargement :'
|
||||||
|
error: Quelque chose s'est mal passé
|
||||||
|
fileInfo: Informations
|
||||||
|
filesSelected: "{count} éléments sélectionnés"
|
||||||
|
lastModified: Dernière modification
|
||||||
|
move: Déplacer
|
||||||
|
moveMessage: 'Choisissez l''emplacement où déplacer la sélection :'
|
||||||
|
newDir: Nouveau dossier
|
||||||
|
newDirMessage: 'Nom du nouveau dossier :'
|
||||||
|
newFile: Nouveau fichier
|
||||||
|
newFileMessage: 'Nom du nouveau fichier :'
|
||||||
|
numberDirs: Nombre de dossiers
|
||||||
|
numberFiles: Nombre de fichiers
|
||||||
|
replace: Remplacer
|
||||||
|
replaceMessage: >
|
||||||
|
Un des fichiers que vous êtes en train d'importer a le même nom qu'un autre déjà présent.
|
||||||
|
Voulez-vous remplacer le fichier actuel par le nouveau ?
|
||||||
|
rename: Renommer
|
||||||
|
renameMessage: Nouveau nom pour
|
||||||
|
show: Montrer
|
||||||
|
size: Taille
|
||||||
|
schedule: Fixer la date
|
||||||
|
scheduleMessage: Choisissez une date pour planifier la publication de ce post
|
||||||
|
newArchetype: Créer un nouveau post basé sur un archétype. Votre fichier sera créé dans le dossier de contenu.
|
||||||
|
settings:
|
||||||
|
admin: Admin
|
||||||
|
administrator: Administrateur
|
||||||
|
allowCommands: Exécuter des commandes
|
||||||
|
allowEdit: Editer, renommer et supprimer des fichiers ou des dossiers
|
||||||
|
allowNew: Créer de nouveaux fichiers et dossiers
|
||||||
|
allowPublish: Publier de nouveaux posts et pages
|
||||||
|
avoidChanges: "(Laisser vide pour conserver l'actuel)"
|
||||||
|
changePassword: Modifier le mot de passe
|
||||||
|
commands: Commandes
|
||||||
|
commandsHelp: >
|
||||||
|
Ici vous pouvez définir des commandes qui seront exécutées lors de l'évènement correspondant.
|
||||||
|
Vous devez indiquer une commande par ligne. Si l'évènement est en rapport avec des fichiers,
|
||||||
|
par exemple avant et après enregistrement, la variable d'environement "FILE" sera disponible
|
||||||
|
et contiendra le chemin d'accès vers le fichier.
|
||||||
|
commandsUpdated: Commandes mises à jour !
|
||||||
|
customStylesheet: Feuille de style personnalisée
|
||||||
|
examples: Exemples
|
||||||
|
globalSettings: Paramètres généraux
|
||||||
|
language: Langue
|
||||||
|
newPassword: Votre nouveau mot de passe
|
||||||
|
newPasswordConfirm: Confirmation du nouveau mot de passe
|
||||||
|
newUser: Nouvel Utilisateur
|
||||||
|
password: Mot de passe
|
||||||
|
passwordUpdated: Mot de passe mis à jour !
|
||||||
|
permissions: Permissions
|
||||||
|
permissionsHelp: >
|
||||||
|
Vous pouvez définir l'utilisateur comme étant un administrateur ou encore choisir les
|
||||||
|
permissions individuellement. Si vous sélectionnez "Administrateur", toutes les autres
|
||||||
|
options seront automatiquement activées. La gestion des utilisateurs est un privilège que
|
||||||
|
seul l'administrateur possède.
|
||||||
|
profileSettings: Paramètres du profil
|
||||||
|
ruleExample1: Bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers
|
||||||
|
ruleExample2: Bloque l'accès au fichier nommé "Caddyfile" à la racine du dossier utilisateur
|
||||||
|
rules: Règles
|
||||||
|
rulesHelp1: >
|
||||||
|
Vous pouvez définir ici un ensemble de règles pour cet utilisateur.
|
||||||
|
Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur.
|
||||||
|
Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur.
|
||||||
|
rulesHelp2: >
|
||||||
|
Chaque règle est définie sur une ligne différente et doit commencer par le mot clé {0} ou {1}.
|
||||||
|
Vous devez ensuite ajouter {2} si vous utilisez une expression régulière puis l'expression en question ou bien seulement le chemin d'accès.
|
||||||
|
scope: Portée du dossier utilisateur
|
||||||
|
settingsUpdated: Les paramètres ont été mis à jour !
|
||||||
|
user: Utilisateur
|
||||||
|
userCommands: Commandes
|
||||||
|
userCommandsHelp: 'Une liste séparée par des espaces des commandes permises pour l''utilisateur. Exemple :'
|
||||||
|
userCreated: Utilisateur créé !
|
||||||
|
userDeleted: Utilisateur supprimé !
|
||||||
|
userManagement: Gestion des utilisateurs
|
||||||
|
username: Nom d'utilisateur
|
||||||
|
users: Utilisateurs
|
||||||
|
userUpdated: Utilisateur mis à jour !
|
||||||
|
sidebar:
|
||||||
|
help: Aide
|
||||||
|
logout: Se déconnecter
|
||||||
|
myFiles: Mes fichiers
|
||||||
|
newFile: Nouveau fichier
|
||||||
|
newFolder: Nouveau dossier
|
||||||
|
settings: Paramètres
|
||||||
|
siteSettings: Paramètres du site
|
||||||
|
hugoNew: Nouveau Hugo
|
||||||
|
preview: Prévisualiser
|
||||||
|
search:
|
||||||
|
images: Images
|
||||||
|
music: Musique
|
||||||
|
pdf: PDF
|
||||||
|
pressToExecute: Appuyez sur Entrée pour exécuter
|
||||||
|
pressToSearch: Appuyez sur Entrée pour lancer la recherche
|
||||||
|
search: Recherche en cours...
|
||||||
|
searchOrCommand: Rechercher ou exécuter une commande...
|
||||||
|
searchOrSupportedCommand: 'Lancez une recherche ou exécutez une commande parmis les suivantes :'
|
||||||
|
type: Tapez votre recherche et appuyez sur Entrée
|
||||||
|
types: Types
|
||||||
|
video: Video
|
||||||
|
writeToSearch: Ecrivez ici pour lancer une recherche
|
||||||
|
languages:
|
||||||
|
en: English
|
||||||
|
fr: Français
|
||||||
|
pt: Português
|
||||||
|
ja: 日本語
|
||||||
|
zhCN: 中文 (简体)
|
||||||
|
zhTW: 中文 (繁體)
|
||||||
|
time:
|
||||||
|
unit: Unité de temps
|
||||||
|
seconds: Secondes
|
||||||
|
minutes: Minutes
|
||||||
|
hours: Heures
|
||||||
|
days: Jours
|
||||||
25
assets/src/i18n/index.js
Normal file
25
assets/src/i18n/index.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VueI18n from 'vue-i18n'
|
||||||
|
import en from './en.yaml'
|
||||||
|
import fr from './fr.yaml'
|
||||||
|
import pt from './pt.yaml'
|
||||||
|
import ja from './ja.yaml'
|
||||||
|
import zhCN from './zh-cn.yaml'
|
||||||
|
import zhTW from './zh-tw.yaml'
|
||||||
|
|
||||||
|
Vue.use(VueI18n)
|
||||||
|
|
||||||
|
const i18n = new VueI18n({
|
||||||
|
locale: 'en',
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
messages: {
|
||||||
|
'en': en,
|
||||||
|
'fr': fr,
|
||||||
|
'pt': pt,
|
||||||
|
'ja': ja,
|
||||||
|
'zh-cn': zhCN,
|
||||||
|
'zh-tw': zhTW
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
200
assets/src/i18n/ja.yaml
Normal file
200
assets/src/i18n/ja.yaml
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
permanent: 永久
|
||||||
|
buttons:
|
||||||
|
cancel: キャンセル
|
||||||
|
close: 閉じる
|
||||||
|
copy: コピー
|
||||||
|
copyFile: ファイルをコピー
|
||||||
|
copyToClipboard: クリップボードにコピー
|
||||||
|
create: 作成
|
||||||
|
delete: 削除
|
||||||
|
download: ダウンロード
|
||||||
|
info: 情報
|
||||||
|
more: More
|
||||||
|
move: 移動
|
||||||
|
moveFile: ファイルを移動
|
||||||
|
new: 新規
|
||||||
|
next: 次
|
||||||
|
ok: OK
|
||||||
|
replace: 置き換える
|
||||||
|
previous: 前
|
||||||
|
rename: 名前を変更
|
||||||
|
reportIssue: 問題を報告
|
||||||
|
save: 保存
|
||||||
|
search: 検索
|
||||||
|
select: 選択
|
||||||
|
share: シェア
|
||||||
|
publish: 発表
|
||||||
|
selectMultiple: 複数選択
|
||||||
|
schedule: スケジュール
|
||||||
|
switchView: 表示を切り替わる
|
||||||
|
toggleSidebar: サイドバーを表示する
|
||||||
|
update: 更新
|
||||||
|
upload: アップロード
|
||||||
|
permalink: 固定リンク
|
||||||
|
success:
|
||||||
|
linkCopied: リンクがコピーされました!
|
||||||
|
errors:
|
||||||
|
forbidden: アクセスが拒否されました。
|
||||||
|
internal: 内部エラーが発生しました。
|
||||||
|
notFound: リソースが見つからなりませんでした。
|
||||||
|
files:
|
||||||
|
folders: フォルダ
|
||||||
|
files: ファイル
|
||||||
|
body: 本文
|
||||||
|
clear: クリアー
|
||||||
|
closePreview: プレビューを閉じる
|
||||||
|
home: ホーム
|
||||||
|
lastModified: 最終変更
|
||||||
|
loading: ローディング...
|
||||||
|
lonely: ここには何もない...
|
||||||
|
metadata: メタデータ
|
||||||
|
multipleSelectionEnabled: 複数選択有効
|
||||||
|
name: 名前
|
||||||
|
size: サイズ
|
||||||
|
sortByName: 名前によるソート
|
||||||
|
sortBySize: サイズによるソート
|
||||||
|
sortByLastModified: 最終変更日付によるソート
|
||||||
|
help:
|
||||||
|
click: ファイルやディレクトリを選択
|
||||||
|
ctrl:
|
||||||
|
click: 複数のファイルやディレクトリを選択
|
||||||
|
f: 検索を有効にする
|
||||||
|
s: ファイルを保存またはカレントディレクトリをダウンロード
|
||||||
|
del: 選択した項目を削除
|
||||||
|
doubleClick: ファイルやディレクトリをオープン
|
||||||
|
esc: 選択をクリアーまたはプロンプトを閉じる
|
||||||
|
f1: このヘルプを表示
|
||||||
|
f2: ファイルの名前を変更
|
||||||
|
help: ヘルプ
|
||||||
|
login:
|
||||||
|
password: パスワード
|
||||||
|
submit: ログイン
|
||||||
|
username: ユーザ名
|
||||||
|
wrongCredentials: ユーザ名またはパスワードが間違っています。
|
||||||
|
prompts:
|
||||||
|
copy: コピー
|
||||||
|
copyMessage: コピーの目標ディレクトリを選択してください:
|
||||||
|
currentlyNavigating: 現在閲覧しているディレクトリ:
|
||||||
|
deleteMessageMultiple: '{count} つのファイルを本当に削除してよろしいですか。'
|
||||||
|
deleteMessageSingle: このファイル/フォルダを本当に削除してよろしいですか。
|
||||||
|
deleteTitle: ファイルを削除
|
||||||
|
displayName: 名前:
|
||||||
|
download: ファイルをダウンロード
|
||||||
|
downloadMessage: 圧縮形式を選択してください。
|
||||||
|
error: あるエラーが発生しました。
|
||||||
|
fileInfo: ファイル情報
|
||||||
|
filesSelected: '{count} つのファイルは選択されました。'
|
||||||
|
lastModified: 最終変更
|
||||||
|
move: 移動
|
||||||
|
moveMessage: 移動の目標ディレクトリを選択してください:
|
||||||
|
newDir: 新しいディレクトリを作成
|
||||||
|
newDirMessage: 新しいディレクトリの名前を入力してください。
|
||||||
|
newFile: 新しいファイルを作成
|
||||||
|
newFileMessage: 新しいファイルの名前を入力してください。
|
||||||
|
numberDirs: ディレクトリ個数
|
||||||
|
numberFiles: ファイル個数
|
||||||
|
replace: 置き換える
|
||||||
|
replaceMessage: >
|
||||||
|
アップロードするファイルの中でかち合う名前が一つあります。
|
||||||
|
既存のファイルを置き換えりませんか。
|
||||||
|
rename: 名前を変更
|
||||||
|
renameMessage: 名前を変更しようファイルは:
|
||||||
|
show: 表示
|
||||||
|
size: サイズ
|
||||||
|
schedule: スケジュール
|
||||||
|
scheduleMessage: このポストの発表日付をスケジュールしてください。
|
||||||
|
newArchetype: ある元型に基づいて新しいポストを作成します。ファイルは コンテンツフォルダに作成されます。
|
||||||
|
settings:
|
||||||
|
admin: 管理者
|
||||||
|
administrator: 管理者
|
||||||
|
allowCommands: コマンドの実行
|
||||||
|
allowEdit: ファイルやディレクトリの編集、名前変更と削除
|
||||||
|
allowNew: ファイルとディレクトリの作成
|
||||||
|
allowPublish: ポストとぺーじの発表
|
||||||
|
avoidChanges: "(変更を避けるために空白にしてください)"
|
||||||
|
changePassword: パスワードを変更
|
||||||
|
commands: コマンド
|
||||||
|
commandsHelp: "\
|
||||||
|
ここで、名前付きイベントに実行するコマンドを設定することができます。\
|
||||||
|
一行にコマンド一つを入力してください。\
|
||||||
|
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\
|
||||||
|
環境変数 FILE はファイルのパスに割り当てられます。"
|
||||||
|
commandsUpdated: コマンドは更新されました!
|
||||||
|
customStylesheet: カスタムスタイルシ ート
|
||||||
|
examples: 例
|
||||||
|
globalSettings: グローバル設定
|
||||||
|
language: 言語
|
||||||
|
lockPassowrd: 新しいパスワードを変更に禁止
|
||||||
|
newPassword: 新しいパスワード
|
||||||
|
newPasswordConfirm: 新しいパスワードを確認します
|
||||||
|
newUser: 新しいユーザー
|
||||||
|
password: パスワード
|
||||||
|
passwordUpdated: パスワードは更新されました!
|
||||||
|
permissions: 権限
|
||||||
|
permissionsHelp: "\
|
||||||
|
あなたはユーザーを管理者に設定し、または権限を個々に設定しできます。\
|
||||||
|
\"管理者\"を選択する場合、その他のすべての選択肢は自動的に設定されます。\
|
||||||
|
ユーザーの管理は管理者の権限として保留されました。"
|
||||||
|
profileSettings: プロファイル設定
|
||||||
|
ruleExample1: "\
|
||||||
|
各フォルダに名前はドットで始まるファイル(例えば、.git、.gitignore)\
|
||||||
|
へのアクセスを制限します。"
|
||||||
|
ruleExample2: 範囲のルートパスに名前は Caddyfile のファイルへのアクセスを制限します。
|
||||||
|
rules: 規則
|
||||||
|
rulesHelp1: "\
|
||||||
|
ここに、あなたはこのユーザーの許可または拒否規則を設定できます。\
|
||||||
|
ブロックされたファイルはリストに表示されません、それではアクセスも制限されます。\
|
||||||
|
正規表現(regex)のサポートと範囲に相対のパスが提供されています。"
|
||||||
|
rulesHelp2: "\
|
||||||
|
一行に規則一つを入力してください、\
|
||||||
|
その間に規則はキーワード {0} や {1} で始める必要があります。\
|
||||||
|
そして正規表現を使う場合、{2} と入力し、表現やパスを入力してください。"
|
||||||
|
scope: 範囲
|
||||||
|
settingsUpdated: 設定は更新されました!
|
||||||
|
user: ユーザー
|
||||||
|
userCommands: ユーザーのコマンド
|
||||||
|
userCommandsHelp: "\
|
||||||
|
空白区切りの有効のコマンドのリストを指定してください。\
|
||||||
|
例:"
|
||||||
|
userCreated: ユーザーは作成されました!
|
||||||
|
userDeleted: ユーザーは削除されました!
|
||||||
|
userManagement: ユーザー管理
|
||||||
|
username: ユーザー名
|
||||||
|
users: ユーザー
|
||||||
|
userUpdated: ユーザーは更新されました!
|
||||||
|
sidebar:
|
||||||
|
help: ヘルプ
|
||||||
|
logout: ログアウト
|
||||||
|
myFiles: 私のファイル
|
||||||
|
newFile: 新しいファイルを作成
|
||||||
|
newFolder: 新しいフォルダを作成
|
||||||
|
settings: 設定
|
||||||
|
siteSettings: サイト設定
|
||||||
|
hugoNew: Hugo New
|
||||||
|
preview: プレビュー
|
||||||
|
search:
|
||||||
|
images: 画像
|
||||||
|
music: 音楽
|
||||||
|
pdf: PDF
|
||||||
|
pressToExecute: Enter を押して実行します。
|
||||||
|
pressToSearch: Enter を押して検索します。
|
||||||
|
search: 検索...
|
||||||
|
searchOrCommand: コマンドを検索または実行します。
|
||||||
|
searchOrSupportedCommand: サポートしているコマンドを検索または実行します:
|
||||||
|
type: キーワードを入力し、Enter を押して検索します。
|
||||||
|
types: 種類
|
||||||
|
video: ビデオ
|
||||||
|
writeToSearch: ここにキーワードを入力してください
|
||||||
|
languages:
|
||||||
|
en: English
|
||||||
|
fr: Français
|
||||||
|
pt: Português
|
||||||
|
ja: 日本語
|
||||||
|
zhCN: 中文 (简体)
|
||||||
|
zhTW: 中文 (繁體)
|
||||||
|
time:
|
||||||
|
unit: 時間単位
|
||||||
|
seconds: 秒
|
||||||
|
minutes: 分
|
||||||
|
hours: 時間
|
||||||
|
days: 日
|
||||||
203
assets/src/i18n/pt.yaml
Normal file
203
assets/src/i18n/pt.yaml
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
permanent: Permanente
|
||||||
|
buttons:
|
||||||
|
cancel: Cancelar
|
||||||
|
close: Fechar
|
||||||
|
copy: Copiar
|
||||||
|
copyFile: Copiar ficheiro
|
||||||
|
copyToClipboard: Copiar
|
||||||
|
create: Criar
|
||||||
|
delete: Eliminar
|
||||||
|
download: Descarregar
|
||||||
|
info: Info
|
||||||
|
more: Mais
|
||||||
|
move: Mover
|
||||||
|
moveFile: Mover ficheiro
|
||||||
|
new: Novo
|
||||||
|
next: Próximo
|
||||||
|
ok: OK
|
||||||
|
previous: Anterior
|
||||||
|
publish: Publicar
|
||||||
|
rename: Renomear
|
||||||
|
replace: Substituir
|
||||||
|
reportIssue: Reportar Erro
|
||||||
|
save: Guardar
|
||||||
|
share: Partilhar
|
||||||
|
schedule: Agendar
|
||||||
|
search: Pesquisar
|
||||||
|
select: Selecionar
|
||||||
|
selectMultiple: Selecionar múltiplos
|
||||||
|
switchView: Alterar modo de visão
|
||||||
|
toggleSidebar: Alternar barra lateral
|
||||||
|
update: Atualizar
|
||||||
|
upload: Enviar
|
||||||
|
permalink: Obter link permanente
|
||||||
|
success:
|
||||||
|
linkCopied: Link copiado!
|
||||||
|
errors:
|
||||||
|
forbidden: Tu não és bem-vindo aqui.
|
||||||
|
internal: Algo correu bastante mal.
|
||||||
|
notFound: Não conseguimos chegar a esta localização.
|
||||||
|
files:
|
||||||
|
body: Corpo
|
||||||
|
clear: Limpar
|
||||||
|
closePreview: Fechar pré-visualização
|
||||||
|
files: Ficheiros
|
||||||
|
folders: Pastas
|
||||||
|
home: Início
|
||||||
|
lastModified: Última modificação
|
||||||
|
loading: A carregar...
|
||||||
|
lonely: Sinto-me sozinho...
|
||||||
|
metadata: Metadados
|
||||||
|
multipleSelectionEnabled: Seleção múltipla ativada
|
||||||
|
name: Nome
|
||||||
|
size: Tamanho
|
||||||
|
sortByLastModified: Ordenar pela última modificação
|
||||||
|
sortByName: Ordenar pelo nome
|
||||||
|
sortBySize: Ordenar pelo tamanho
|
||||||
|
help:
|
||||||
|
click: selecionar pasta ou ficheiro
|
||||||
|
ctrl:
|
||||||
|
click: selecionar várias pastas e ficheiros
|
||||||
|
f: pesquisar
|
||||||
|
s: guardar um ficheiro ou descarregar a pasta em que estás a navegar
|
||||||
|
del: eliminar os ficheiros selecionados
|
||||||
|
doubleClick: abrir pasta ou ficheiro
|
||||||
|
esc: limpar seleção e/ou fechar menu
|
||||||
|
f1: esta informação
|
||||||
|
f2: renomear ficheiro
|
||||||
|
help: Ajuda
|
||||||
|
languages:
|
||||||
|
en: English
|
||||||
|
fr: Français
|
||||||
|
pt: Português
|
||||||
|
ja: 日本語
|
||||||
|
zhCN: 中文 (简体)
|
||||||
|
zhTW: 中文 (繁體)
|
||||||
|
login:
|
||||||
|
password: Palavra-passe
|
||||||
|
submit: Login
|
||||||
|
username: Nome de utilizador
|
||||||
|
wrongCredentials: Dados errados
|
||||||
|
prompts:
|
||||||
|
copy: Copiar
|
||||||
|
copyMessage: 'Escolhe um lugar para copiar os ficheiros:'
|
||||||
|
currentlyNavigating: 'A navegar em:'
|
||||||
|
deleteMessageMultiple: Deseja eliminar {count} ficheiro(s)?
|
||||||
|
deleteMessageSingle: Deseja eliminar esta pasta/ficheiro?
|
||||||
|
deleteTitle: Eliminar ficheiros
|
||||||
|
displayName: 'Nome:'
|
||||||
|
download: Descarregar ficheiros
|
||||||
|
downloadMessage: Escolha o formato do ficheiro.
|
||||||
|
error: Algo correu mal
|
||||||
|
fileInfo: Informação do ficheiro
|
||||||
|
filesSelected: "{count} ficheiros selecionados."
|
||||||
|
lastModified: Última Modificação
|
||||||
|
move: Mover
|
||||||
|
moveMessage: 'Escolha uma nova casa para os seus ficheiros:'
|
||||||
|
newArchetype: Criar um novo post baseado num "archetype". O seu ficheiro será criado
|
||||||
|
na pasta "content".
|
||||||
|
newDir: Nova pasta
|
||||||
|
newDirMessage: Escreva o nome da nova pasta.
|
||||||
|
newFile: Novo ficheiro
|
||||||
|
newFileMessage: Escreva o nome do novo ficheiro.
|
||||||
|
numberDirs: Número de pastas
|
||||||
|
numberFiles: Número de ficheiros
|
||||||
|
rename: Renomear
|
||||||
|
renameMessage: Insira um novo nome para
|
||||||
|
replace: Substituir
|
||||||
|
replaceMessage: >
|
||||||
|
Já existe um ficheiro com nome igual a um dos que está a tentar
|
||||||
|
enviar. Deseja substituir?
|
||||||
|
schedule: Agendar
|
||||||
|
scheduleMessage: Escolha uma data para publicar este post.
|
||||||
|
show: Mostrar
|
||||||
|
size: Tamanho
|
||||||
|
search:
|
||||||
|
images: Imagens
|
||||||
|
music: Música
|
||||||
|
pdf: PDF
|
||||||
|
pressToExecute: Prima enter para executar.
|
||||||
|
pressToSearch: Prima enter para pesquisar.
|
||||||
|
search: Pesquise...
|
||||||
|
searchOrCommand: Pesquise ou execute um comando...
|
||||||
|
searchOrSupportedCommand: 'Pesquise ou utilize um dos seus comandos:'
|
||||||
|
type: Escreva e prima enter para pesquisar.
|
||||||
|
types: Tipos
|
||||||
|
video: Vídeos
|
||||||
|
writeToSearch: Escreva aqui para pesquisar
|
||||||
|
settings:
|
||||||
|
admin: Admin
|
||||||
|
administrator: Administrador
|
||||||
|
allowCommands: Executar comandos
|
||||||
|
allowEdit: Editar, renomear e eliminar ficheiros ou pastas
|
||||||
|
allowNew: Criar novos ficheiros e pastas
|
||||||
|
allowPublish: Publicar novas páginas e conteúdos
|
||||||
|
avoidChanges: "(deixe em branco para manter)"
|
||||||
|
changePassword: Alterar Password
|
||||||
|
commands: Comandos
|
||||||
|
commandsHelp: >
|
||||||
|
Pode definir um conjunto de comandos a executar em determiandos eventos.
|
||||||
|
Deve escrever um comando por linha. Se o evento estiver relacionado com ficheiros,
|
||||||
|
como antes e depois de guardar, irá existir uma variável de ambiente denominada
|
||||||
|
"FILE" com o caminho do ficheiro.
|
||||||
|
commandsUpdated: Comandos atualizados!
|
||||||
|
customStylesheet: Estilos Personalizados
|
||||||
|
examples: Exemplos
|
||||||
|
globalSettings: Configurações Globais
|
||||||
|
language: Linguagem
|
||||||
|
lockPassword: Não permitir que o utilizador altere a palavra-passe
|
||||||
|
newPassword: Nova palavra-passe
|
||||||
|
newPasswordConfirm: Confirme a nova palavra-passe
|
||||||
|
newUser: Novo Utilizador
|
||||||
|
password: Palavra-passe
|
||||||
|
passwordUpdated: Palavra-passe atualizada!
|
||||||
|
permissions: Permissões
|
||||||
|
permissionsHelp: >
|
||||||
|
Pode definir o utilizador como administrador ou escolher as permissões
|
||||||
|
manualmente. Se selecionar a opção "Administrador", todas as outras opções serão
|
||||||
|
automaticamente selecionadas. A gestão dos utilizadores é um privilégio restringido
|
||||||
|
aos administradores.
|
||||||
|
profileSettings: Configurações do Utilizador
|
||||||
|
ruleExample1: >
|
||||||
|
previne o acesso a qualquer "dotfile" (como .git, .gitignore) em
|
||||||
|
qualquer pasta
|
||||||
|
ruleExample2: bloqueia o acesso ao ficheiro chamado Caddyfile.
|
||||||
|
rules: Regras
|
||||||
|
rulesHelp1: >
|
||||||
|
Aqui pode definir um conjunto de regras para permitir ou bloquear o
|
||||||
|
acesso do utilizador a determinados ficheiros ou pastas. Os ficheiros bloqueados
|
||||||
|
não irão aparecer durante a navegação. Suportamos expressões regulares e os caminhos
|
||||||
|
dos ficheiros devem ser relativos à base do utilizador.
|
||||||
|
rulesHelp2: >
|
||||||
|
Cada regra deve ser colocada numa linha diferente e deve começar com
|
||||||
|
as palavras {0} (permite) ou {1} (bloqueia). Deve escrever, logo de seguida, {2},
|
||||||
|
caso queira utilizar uma expressão regular. Depois, escreva o caminho do ficheiro/pasta
|
||||||
|
ou a expressão regular.
|
||||||
|
scope: Base
|
||||||
|
settingsUpdated: Configurações atualizadas!
|
||||||
|
user: Utilizador
|
||||||
|
userCommands: Comandos
|
||||||
|
userCommandsHelp: 'Uma lista, separada com espaços, de comandos disponíveis para
|
||||||
|
este utilizados. Exemplo:'
|
||||||
|
userCreated: Utilizador criado!
|
||||||
|
userDeleted: Utilizador eliminado!
|
||||||
|
userManagement: Gestão de Utilizadores
|
||||||
|
username: Nome de utilizador
|
||||||
|
users: Utilizadores
|
||||||
|
userUpdated: Utilizador atualizado!
|
||||||
|
sidebar:
|
||||||
|
help: Ajuda
|
||||||
|
hugoNew: Hugo New
|
||||||
|
logout: Sair
|
||||||
|
myFiles: Ficheiros
|
||||||
|
newFile: Novo ficheiro
|
||||||
|
newFolder: Nova pasta
|
||||||
|
preview: Pré-visualizar
|
||||||
|
settings: Configurações
|
||||||
|
siteSettings: Configurações do Site
|
||||||
|
time:
|
||||||
|
unit: Unidades de Tempo
|
||||||
|
seconds: Segundos
|
||||||
|
minutes: Minutos
|
||||||
|
hours: Horas
|
||||||
|
days: Dias
|
||||||
198
assets/src/i18n/zh-cn.yaml
Normal file
198
assets/src/i18n/zh-cn.yaml
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
permanent: 永久
|
||||||
|
buttons:
|
||||||
|
cancel: 取消
|
||||||
|
close: 关闭
|
||||||
|
copy: 复制
|
||||||
|
copyFile: 复制文件
|
||||||
|
copyToClipboard: 复制到剪贴板
|
||||||
|
create: 创建
|
||||||
|
delete: 删除
|
||||||
|
download: 下载
|
||||||
|
info: 信息
|
||||||
|
more: 更多
|
||||||
|
move: 移动
|
||||||
|
moveFile: 移动文件
|
||||||
|
new: 新
|
||||||
|
next: 下一个
|
||||||
|
ok: 确定
|
||||||
|
replace: 替换
|
||||||
|
previous: 上一个
|
||||||
|
rename: 重命名
|
||||||
|
reportIssue: 报告问题
|
||||||
|
save: 保存
|
||||||
|
search: 搜索
|
||||||
|
select: 选择
|
||||||
|
share: 分享
|
||||||
|
publish: 发布
|
||||||
|
selectMultiple: 选择多个
|
||||||
|
schedule: 计划
|
||||||
|
switchView: 切换显示方式
|
||||||
|
toggleSidebar: 切换侧边栏
|
||||||
|
update: 更新
|
||||||
|
upload: 上传
|
||||||
|
permalink: 获取永久链接
|
||||||
|
success:
|
||||||
|
linkCopied: 链接已复制!
|
||||||
|
errors:
|
||||||
|
forbidden: 你被禁止访问。
|
||||||
|
internal: 内部出现麻烦了。
|
||||||
|
notFound: 找不到文件。
|
||||||
|
files:
|
||||||
|
folders: 文件夹
|
||||||
|
files: 文件
|
||||||
|
body: Body
|
||||||
|
clear: 清空
|
||||||
|
closePreview: 关闭预览
|
||||||
|
home: 主页
|
||||||
|
lastModified: 最后修改
|
||||||
|
loading: 加载中...
|
||||||
|
lonely: 这里没有任何文件...
|
||||||
|
metadata: 元数据
|
||||||
|
multipleSelectionEnabled: 多选模式已开启
|
||||||
|
name: 名称
|
||||||
|
size: 大小
|
||||||
|
sortByName: 按名称排序
|
||||||
|
sortBySize: 按大小排序
|
||||||
|
sortByLastModified: 按最后修改时间排序
|
||||||
|
help:
|
||||||
|
click: 选择文件或目录
|
||||||
|
ctrl:
|
||||||
|
click: 选择多个文件或目录
|
||||||
|
f: 打开搜索框
|
||||||
|
s: 保存文件或下载当前文件夹
|
||||||
|
del: 删除所选的文件/文件夹
|
||||||
|
doubleClick: 打开文件/文件夹
|
||||||
|
esc: 清除已选项或关闭提示信息
|
||||||
|
f1: 显示该帮助信息
|
||||||
|
f2: 重命名文件/文件夹
|
||||||
|
help: 帮助
|
||||||
|
login:
|
||||||
|
password: 密码
|
||||||
|
submit: 登录
|
||||||
|
username: 用户名
|
||||||
|
wrongCredentials: 用户名或密码错误
|
||||||
|
prompts:
|
||||||
|
copy: 复制
|
||||||
|
copyMessage: 请选择欲复制至的目录:
|
||||||
|
currentlyNavigating: 当前目录:
|
||||||
|
deleteMessageMultiple: 你确定要删除这 {count} 个文件吗?
|
||||||
|
deleteMessageSingle: 你确定要删除这个文件/文件夹吗?
|
||||||
|
deleteTitle: 删除文件
|
||||||
|
displayName: 名称:
|
||||||
|
download: 下载文件
|
||||||
|
downloadMessage: 请选择要下载的压缩格式。
|
||||||
|
error: 出了一点问题...
|
||||||
|
fileInfo: 文件信息
|
||||||
|
filesSelected: 已选择 {count} 个文件。
|
||||||
|
lastModified: 最后修改
|
||||||
|
move: 移动
|
||||||
|
moveMessage: 请选择欲移动至的目录:
|
||||||
|
newDir: 新建目录
|
||||||
|
newDirMessage: 请输入新目录的名称。
|
||||||
|
newFile: 新建文件
|
||||||
|
newFileMessage: 请输入新文件的名称。
|
||||||
|
numberDirs: 目录数
|
||||||
|
numberFiles: 文件数
|
||||||
|
replace: 替换
|
||||||
|
replaceMessage: "\
|
||||||
|
您尝试上传的文件中有一个与现有文件的名称存在冲突。\
|
||||||
|
是否替换现有的同名文件?"
|
||||||
|
rename: 重命名
|
||||||
|
renameMessage: 请输入新名称,旧名称为:
|
||||||
|
show: 揭示
|
||||||
|
size: 大小
|
||||||
|
schedule: 计划
|
||||||
|
scheduleMessage: 请选择发布这篇帖子的日期。
|
||||||
|
newArchetype: 创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。
|
||||||
|
settings:
|
||||||
|
admin: 管理员
|
||||||
|
administrator: 管理员
|
||||||
|
allowCommands: 执行命令(Linux 代码)
|
||||||
|
allowEdit: 编辑、重命名或删除文件/目录
|
||||||
|
allowNew: 创建新文件和目录
|
||||||
|
allowPublish: 发布新的帖子与页面
|
||||||
|
avoidChanges: '(留空以避免更改)'
|
||||||
|
changePassword: 更改密码
|
||||||
|
commands: 命令(linux 代码)
|
||||||
|
commandsHelp: "\
|
||||||
|
在这里,您可以设置在指定事件下执行的命令,一行一条。\
|
||||||
|
若事件与文件相关,如“在保存文件前”,\
|
||||||
|
则文件的路径会被赋值给环境变量 \"FILE\"。"
|
||||||
|
commandsUpdated: 命令已更新!
|
||||||
|
customStylesheet: 自定义样式表
|
||||||
|
examples: 例子
|
||||||
|
globalSettings: 全局设置
|
||||||
|
language: 语言
|
||||||
|
lockPassowrd: 禁止用户修改密码
|
||||||
|
newPassword: 您的新密码
|
||||||
|
newPasswordConfirm: 重输一遍新密码
|
||||||
|
newUser: 新建用户
|
||||||
|
password: 密码
|
||||||
|
passwordUpdated: 密码已更新!
|
||||||
|
permissions: 权限
|
||||||
|
permissionsHelp: "\
|
||||||
|
您可以将该用户设置为管理员,也可以单独选择各项权限。\
|
||||||
|
如果选择了“管理员”,则其他的选项会被自动勾上,\
|
||||||
|
同时该用户可以管理其他用户。"
|
||||||
|
profileSettings: 配置文件设置
|
||||||
|
ruleExample1: "\
|
||||||
|
阻止用户访问所有文件夹下任何以 . 开头的文件\
|
||||||
|
(隐藏文件, 例如: .git, .gitignore)。"
|
||||||
|
ruleExample2: 阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。
|
||||||
|
rules: 规则
|
||||||
|
rulesHelp1: "\
|
||||||
|
您可以为该用户制定一组黑名单或白名单式的规则,\
|
||||||
|
被屏蔽的文件将不会显示在列表中,用户也无权限访问,\
|
||||||
|
支持相对于目录范围的路径。"
|
||||||
|
rulesHelp2: "\
|
||||||
|
每行一条规则,且必须以关键词 {0} 或 {1} 开头。\
|
||||||
|
如要使用正则表达式,请在加上 {2} 之后再附上表达式或路径。"
|
||||||
|
scope: 目录范围
|
||||||
|
settingsUpdated: 设置已更新!
|
||||||
|
user: 用户
|
||||||
|
userCommands: 用户命令(Linux 代码)
|
||||||
|
userCommandsHelp: "\
|
||||||
|
指定该用户可以执行的命令(Linux 代码),用空格分隔。\
|
||||||
|
例如:"
|
||||||
|
userCreated: 用户已创建!
|
||||||
|
userDeleted: 用户已删除!
|
||||||
|
userManagement: 用户管理
|
||||||
|
username: 用户名
|
||||||
|
users: 用户
|
||||||
|
userUpdated: 用户已更新!
|
||||||
|
sidebar:
|
||||||
|
help: 帮助
|
||||||
|
logout: 登出
|
||||||
|
myFiles: 我的文件
|
||||||
|
newFile: 新建文件
|
||||||
|
newFolder: 新建文件夹
|
||||||
|
settings: 设置
|
||||||
|
siteSettings: 网站设置
|
||||||
|
hugoNew: Hugo New
|
||||||
|
preview: 预览
|
||||||
|
search:
|
||||||
|
images: 图像
|
||||||
|
music: 音乐
|
||||||
|
pdf: PDF
|
||||||
|
pressToExecute: 按回车键执行。
|
||||||
|
pressToSearch: 按回车键搜索。
|
||||||
|
search: 搜索...
|
||||||
|
searchOrCommand: 搜索或者执行命令(Linux 代码)...
|
||||||
|
searchOrSupportedCommand: 搜索或使用您可以使用的命令(一次只能执行一个命令):
|
||||||
|
type: 键入并按回车键进行搜索。
|
||||||
|
types: 类型
|
||||||
|
video: 视频
|
||||||
|
writeToSearch: 请输入要搜索的内容
|
||||||
|
languages:
|
||||||
|
en: English
|
||||||
|
fr: Français
|
||||||
|
pt: Português
|
||||||
|
ja: 日本語
|
||||||
|
zhCN: 中文 (简体)
|
||||||
|
zhTW: 中文 (繁體)
|
||||||
|
time:
|
||||||
|
unit: 时间单位
|
||||||
|
seconds: 秒
|
||||||
|
minutes: 分钟
|
||||||
|
hours: 小时
|
||||||
|
days: 天
|
||||||
198
assets/src/i18n/zh-tw.yaml
Normal file
198
assets/src/i18n/zh-tw.yaml
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
permanent: 永久
|
||||||
|
buttons:
|
||||||
|
cancel: 取消
|
||||||
|
close: 關閉
|
||||||
|
copy: 複製
|
||||||
|
copyFile: 複製檔案
|
||||||
|
copyToClipboard: 複製到剪貼簿
|
||||||
|
create: 建立
|
||||||
|
delete: 刪除
|
||||||
|
download: 下載
|
||||||
|
info: 資訊
|
||||||
|
more: 更多
|
||||||
|
move: 移動
|
||||||
|
moveFile: 移動檔案
|
||||||
|
new: 新
|
||||||
|
next: 下一個
|
||||||
|
ok: 確認
|
||||||
|
replace: 更換
|
||||||
|
previous: 上一個
|
||||||
|
rename: 重新命名
|
||||||
|
reportIssue: 報告問題
|
||||||
|
save: 儲存
|
||||||
|
search: 搜尋
|
||||||
|
select: 選擇
|
||||||
|
share: 分享
|
||||||
|
publish: 發佈
|
||||||
|
selectMultiple: 選擇多個
|
||||||
|
schedule: 計畫
|
||||||
|
switchView: 切換顯示方式
|
||||||
|
toggleSidebar: 切換側邊欄
|
||||||
|
update: 更新
|
||||||
|
upload: 上傳
|
||||||
|
permalink: 獲取永久連結
|
||||||
|
success:
|
||||||
|
linkCopied: 連結已複製!
|
||||||
|
errors:
|
||||||
|
forbidden: 你被禁止存取。
|
||||||
|
internal: 內部出現麻煩了。
|
||||||
|
notFound: 找不到檔案。
|
||||||
|
files:
|
||||||
|
folders: 資料夾
|
||||||
|
files: 檔案
|
||||||
|
body: Body
|
||||||
|
clear: 清空
|
||||||
|
closePreview: 關閉預覽
|
||||||
|
home: 主頁
|
||||||
|
lastModified: 最後修改
|
||||||
|
loading: 讀取中...
|
||||||
|
lonely: 這裡沒有任何檔案...
|
||||||
|
metadata: 詮釋資料
|
||||||
|
multipleSelectionEnabled: 多選模式已開啟
|
||||||
|
name: 名稱
|
||||||
|
size: 大小
|
||||||
|
sortByName: 按名稱排序
|
||||||
|
sortBySize: 按大小排序
|
||||||
|
sortByLastModified: 按最後修改時間排序
|
||||||
|
help:
|
||||||
|
click: 選擇檔案或目錄
|
||||||
|
ctrl:
|
||||||
|
click: 選擇多個檔案或目錄
|
||||||
|
f: 打開搜尋列
|
||||||
|
s: 儲存檔案或下載目前資料夾
|
||||||
|
del: 刪除所選的檔案/資料夾
|
||||||
|
doubleClick: 打開檔案/資料夾
|
||||||
|
esc: 清除已選項或關閉提示資訊
|
||||||
|
f1: 顯示該幫助資訊
|
||||||
|
f2: 重新命名檔案/資料夾
|
||||||
|
help: 幫助
|
||||||
|
login:
|
||||||
|
password: 密碼
|
||||||
|
submit: 登入
|
||||||
|
username: 帳號
|
||||||
|
wrongCredentials: 帳號或密碼錯誤
|
||||||
|
prompts:
|
||||||
|
copy: 複製
|
||||||
|
copyMessage: 請選擇欲複製至的目錄:
|
||||||
|
currentlyNavigating: 目前目錄:
|
||||||
|
deleteMessageMultiple: 你確定要刪除這 {count} 個檔案嗎?
|
||||||
|
deleteMessageSingle: 你確定要刪除這個檔案/資料夾嗎?
|
||||||
|
deleteTitle: 刪除檔案
|
||||||
|
displayName: 名稱:
|
||||||
|
download: 下載檔案
|
||||||
|
downloadMessage: 請選擇要下載的壓縮格式。
|
||||||
|
error: 發出了一點錯誤...
|
||||||
|
fileInfo: 檔案資訊
|
||||||
|
filesSelected: 已選擇 {count} 個檔案。
|
||||||
|
lastModified: 最後修改
|
||||||
|
move: 移動
|
||||||
|
moveMessage: 請選擇欲移動至的目錄:
|
||||||
|
newDir: 建立目錄
|
||||||
|
newDirMessage: 請輸入新目錄的名稱。
|
||||||
|
newFile: 建立檔案
|
||||||
|
newFileMessage: 請輸入新檔案的名稱。
|
||||||
|
numberDirs: 目錄數
|
||||||
|
numberFiles: 檔案數
|
||||||
|
replace: 替換
|
||||||
|
replaceMessage: "\
|
||||||
|
您嘗試上傳的檔案中有一個與現有檔案的名稱存在衝突。\
|
||||||
|
是否取代現有的同名檔案?"
|
||||||
|
rename: 重新命名
|
||||||
|
renameMessage: 請輸入新名稱,舊名稱為:
|
||||||
|
show: 顯示
|
||||||
|
size: 大小
|
||||||
|
schedule: 計畫
|
||||||
|
scheduleMessage: 請選擇發佈這篇貼文的日期。
|
||||||
|
newArchetype: 建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。
|
||||||
|
settings:
|
||||||
|
admin: 管理員
|
||||||
|
administrator: 管理員
|
||||||
|
allowCommands: 執行命令
|
||||||
|
allowEdit: 編輯、重命名或刪除檔案/目錄
|
||||||
|
allowNew: 創建新檔案和目錄
|
||||||
|
allowPublish: 發佈新的貼文與頁面
|
||||||
|
avoidChanges: '(留空以避免更改)'
|
||||||
|
changePassword: 更改密碼
|
||||||
|
commands: 命令
|
||||||
|
commandsHelp: "\
|
||||||
|
在這裡,您可以設定在指定事件下執行的命令,一行一條。\
|
||||||
|
若事件與檔案相關,如“在保存檔案前”,\
|
||||||
|
則檔案的路徑會被賦值給環境變數 \"FILE\"。"
|
||||||
|
commandsUpdated: 命令已更新!
|
||||||
|
customStylesheet: 自定義樣式表
|
||||||
|
examples: 範例
|
||||||
|
globalSettings: 全域設定
|
||||||
|
language: 語言
|
||||||
|
lockPassword: 禁止使用者修改密碼
|
||||||
|
newPassword: 您的新密碼
|
||||||
|
newPasswordConfirm: 重輸一遍新密碼
|
||||||
|
newUser: 建立使用者
|
||||||
|
password: 密碼
|
||||||
|
passwordUpdated: 密碼已更新!
|
||||||
|
permissions: 權限
|
||||||
|
permissionsHelp: "\
|
||||||
|
您可以將該使用者設置為管理員,也可以單獨選擇各項權限。\
|
||||||
|
如果選擇了“管理員”,則其他的選項會被自動勾上,\
|
||||||
|
同時該使用者可以管理其他使用者。"
|
||||||
|
profileSettings: 設定檔設定
|
||||||
|
ruleExample1: "\
|
||||||
|
封鎖使用者存取所有資料夾下任何以 . 開頭的檔案\
|
||||||
|
(隱藏文件, 例如: .git, .gitignore)。"
|
||||||
|
ruleExample2: 封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。
|
||||||
|
rules: 規則
|
||||||
|
rulesHelp1: "\
|
||||||
|
您可以為該使用者製定一組黑名單或白名單式的規則,\
|
||||||
|
被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,\
|
||||||
|
支持相對於目錄範圍的路徑。"
|
||||||
|
rulesHelp2: "\
|
||||||
|
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
|
||||||
|
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
|
||||||
|
scope: 目錄範圍
|
||||||
|
settingsUpdated: 設定已更新!
|
||||||
|
user: 使用者
|
||||||
|
userCommands: 使用者命令
|
||||||
|
userCommandsHelp: "\
|
||||||
|
指定該使用者可以執行的命令,用空格分隔。\
|
||||||
|
例如:"
|
||||||
|
userCreated: 使用者已建立!
|
||||||
|
userDeleted: 使用者已刪除!
|
||||||
|
userManagement: 使用者管理
|
||||||
|
username: 使用者名稱
|
||||||
|
users: 使用者
|
||||||
|
userUpdated: 使用者已更新!
|
||||||
|
sidebar:
|
||||||
|
help: 幫助
|
||||||
|
logout: 登出
|
||||||
|
myFiles: 我的檔案
|
||||||
|
newFile: 建立檔案
|
||||||
|
newFolder: 建立資料夾
|
||||||
|
settings: 設定
|
||||||
|
siteSettings: 網站設定
|
||||||
|
hugoNew: Hugo New
|
||||||
|
preview: 預覽
|
||||||
|
search:
|
||||||
|
images: 影像
|
||||||
|
music: 音樂
|
||||||
|
pdf: PDF
|
||||||
|
pressToExecute: 按確定鍵執行。
|
||||||
|
pressToSearch: 按確定鍵搜尋。
|
||||||
|
search: 搜尋...
|
||||||
|
searchOrCommand: 搜尋或者執行命令...
|
||||||
|
searchOrSupportedCommand: 搜尋或使用您可以使用的命令(一次只能執行一個命令):
|
||||||
|
type: 輸入並按確定鍵進行搜尋。
|
||||||
|
types: 類型
|
||||||
|
video: 影片
|
||||||
|
writeToSearch: 請輸入要搜尋的內容
|
||||||
|
languages:
|
||||||
|
en: English
|
||||||
|
fr: Français
|
||||||
|
pt: Português
|
||||||
|
ja: 日本語
|
||||||
|
zhCN: 中文 (简体)
|
||||||
|
zhTW: 中文 (繁體)
|
||||||
|
time:
|
||||||
|
unit: 時間單位
|
||||||
|
seconds: 秒
|
||||||
|
minutes: 分鐘
|
||||||
|
hours: 小時
|
||||||
|
days: 天
|
||||||
@@ -2,14 +2,53 @@ import Vue from 'vue'
|
|||||||
import App from './App'
|
import App from './App'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import i18n from './i18n'
|
||||||
|
import Noty from 'noty'
|
||||||
|
|
||||||
Vue.config.productionTip = true
|
Vue.config.productionTip = true
|
||||||
|
|
||||||
|
const notyDefault = {
|
||||||
|
type: 'info',
|
||||||
|
layout: 'bottomRight',
|
||||||
|
timeout: 1000,
|
||||||
|
progressBar: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$noty = function (opts) {
|
||||||
|
new Noty(Object.assign({}, notyDefault, opts)).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$showSuccess = function (message) {
|
||||||
|
new Noty(Object.assign({}, notyDefault, {
|
||||||
|
text: message,
|
||||||
|
type: 'success'
|
||||||
|
})).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$showError = function (error) {
|
||||||
|
let n = new Noty(Object.assign({}, notyDefault, {
|
||||||
|
text: error,
|
||||||
|
type: 'error',
|
||||||
|
timeout: null,
|
||||||
|
buttons: [
|
||||||
|
Noty.button(i18n.t('buttons.reportIssue'), 'cancel', function () {
|
||||||
|
window.open('https://github.com/hacdias/filemanager/issues/new')
|
||||||
|
}),
|
||||||
|
Noty.button(i18n.t('buttons.close'), '', function () {
|
||||||
|
n.close()
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
n.show()
|
||||||
|
}
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
store,
|
store,
|
||||||
router,
|
router,
|
||||||
|
i18n,
|
||||||
template: '<App/>',
|
template: '<App/>',
|
||||||
components: { App }
|
components: { App }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
import Login from '@/components/Login'
|
import Login from '@/views/Login'
|
||||||
import Main from '@/components/Main'
|
import Layout from '@/views/Layout'
|
||||||
import Files from '@/components/Files'
|
import Files from '@/views/Files'
|
||||||
import Users from '@/components/Users'
|
import Users from '@/views/Settings/Users'
|
||||||
import User from '@/components/User'
|
import User from '@/views/Settings/User'
|
||||||
import GlobalSettings from '@/components/GlobalSettings'
|
import Settings from '@/views/Settings'
|
||||||
import ProfileSettings from '@/components/ProfileSettings'
|
import GlobalSettings from '@/views/settings/Global'
|
||||||
import error403 from '@/components/errors/403'
|
import ProfileSettings from '@/views/settings/Profile'
|
||||||
import error404 from '@/components/errors/404'
|
import Error403 from '@/views/errors/403'
|
||||||
import error500 from '@/components/errors/500'
|
import Error404 from '@/views/errors/404'
|
||||||
|
import Error500 from '@/views/errors/500'
|
||||||
import auth from '@/utils/auth.js'
|
import auth from '@/utils/auth.js'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
|
|
||||||
@@ -25,24 +26,18 @@ const router = new Router({
|
|||||||
component: Login,
|
component: Login,
|
||||||
beforeEnter: function (to, from, next) {
|
beforeEnter: function (to, from, next) {
|
||||||
auth.loggedIn()
|
auth.loggedIn()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
next({ path: '/files' })
|
next({ path: '/files' })
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
document.title = 'Login'
|
document.title = 'Login'
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
redirect: {
|
|
||||||
path: '/files/'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/*',
|
path: '/*',
|
||||||
component: Main,
|
component: Layout,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
},
|
},
|
||||||
@@ -55,58 +50,65 @@ const router = new Router({
|
|||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
redirect: {
|
component: Settings,
|
||||||
path: '/settings/profile'
|
children: [
|
||||||
}
|
{
|
||||||
},
|
path: '/settings',
|
||||||
{
|
name: 'Settings',
|
||||||
path: '/settings/profile',
|
redirect: {
|
||||||
name: 'Profile Settings',
|
path: '/settings/profile'
|
||||||
component: ProfileSettings
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/global',
|
path: '/settings/profile',
|
||||||
name: 'Global Settings',
|
name: 'Profile Settings',
|
||||||
component: GlobalSettings,
|
component: ProfileSettings
|
||||||
meta: {
|
},
|
||||||
requiresAdmin: true
|
{
|
||||||
}
|
path: '/settings/global',
|
||||||
|
name: 'Global Settings',
|
||||||
|
component: GlobalSettings,
|
||||||
|
meta: {
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/users',
|
||||||
|
name: 'Users',
|
||||||
|
component: Users,
|
||||||
|
meta: {
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/users/*',
|
||||||
|
name: 'User',
|
||||||
|
component: User,
|
||||||
|
meta: {
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/403',
|
path: '/403',
|
||||||
name: 'Forbidden',
|
name: 'Forbidden',
|
||||||
component: error403
|
component: Error403
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/404',
|
path: '/404',
|
||||||
name: 'Not Found',
|
name: 'Not Found',
|
||||||
component: error404
|
component: Error404
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/500',
|
path: '/500',
|
||||||
name: 'Internal Server Error',
|
name: 'Internal Server Error',
|
||||||
component: error500
|
component: Error500
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/users',
|
path: '/files',
|
||||||
name: 'Users',
|
|
||||||
component: Users,
|
|
||||||
meta: {
|
|
||||||
requiresAdmin: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/users/',
|
|
||||||
redirect: {
|
redirect: {
|
||||||
path: '/users'
|
path: '/files/'
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/users/*',
|
|
||||||
name: 'User',
|
|
||||||
component: User,
|
|
||||||
meta: {
|
|
||||||
requiresAdmin: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,19 +8,29 @@ Vue.use(Vuex)
|
|||||||
const state = {
|
const state = {
|
||||||
user: {},
|
user: {},
|
||||||
req: {},
|
req: {},
|
||||||
plugins: window.plugins || [],
|
|
||||||
clipboard: {
|
clipboard: {
|
||||||
key: '',
|
key: '',
|
||||||
items: []
|
items: []
|
||||||
},
|
},
|
||||||
|
css: (() => {
|
||||||
|
let css = window.CSS
|
||||||
|
window.CSS = null
|
||||||
|
return css
|
||||||
|
})(),
|
||||||
|
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
|
||||||
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
|
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
|
||||||
|
noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'),
|
||||||
|
version: document.querySelector('meta[name="version"]').getAttribute('content'),
|
||||||
jwt: '',
|
jwt: '',
|
||||||
|
progress: 0,
|
||||||
|
schedule: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
reload: false,
|
reload: false,
|
||||||
selected: [],
|
selected: [],
|
||||||
multiple: false,
|
multiple: false,
|
||||||
show: null,
|
show: null,
|
||||||
showMessage: null
|
showMessage: null,
|
||||||
|
showConfirm: null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import i18n from '@/i18n'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
closeHovers: state => {
|
closeHovers: state => {
|
||||||
state.show = null
|
state.show = null
|
||||||
@@ -11,6 +14,7 @@ const mutations = {
|
|||||||
|
|
||||||
state.show = value.prompt
|
state.show = value.prompt
|
||||||
state.showMessage = value.message
|
state.showMessage = value.message
|
||||||
|
state.showConfirm = value.confirm
|
||||||
},
|
},
|
||||||
showError: (state, value) => {
|
showError: (state, value) => {
|
||||||
state.show = 'error'
|
state.show = 'error'
|
||||||
@@ -22,11 +26,18 @@ const mutations = {
|
|||||||
},
|
},
|
||||||
setLoading: (state, value) => { state.loading = value },
|
setLoading: (state, value) => { state.loading = value },
|
||||||
setReload: (state, value) => { state.reload = value },
|
setReload: (state, value) => { state.reload = value },
|
||||||
setUser: (state, value) => (state.user = value),
|
setUser: (state, value) => {
|
||||||
setUserCSS: (state, value) => (state.user.css = value),
|
moment.locale(value.locale)
|
||||||
|
i18n.locale = value.locale
|
||||||
|
state.user = value
|
||||||
|
},
|
||||||
|
setCSS: (state, value) => (state.css = value),
|
||||||
setJWT: (state, value) => (state.jwt = value),
|
setJWT: (state, value) => (state.jwt = value),
|
||||||
multiple: (state, value) => (state.multiple = value),
|
multiple: (state, value) => (state.multiple = value),
|
||||||
addSelected: (state, value) => (state.selected.push(value)),
|
addSelected: (state, value) => (state.selected.push(value)),
|
||||||
|
addPlugin: (state, value) => {
|
||||||
|
state.plugins.push(value)
|
||||||
|
},
|
||||||
removeSelected: (state, value) => {
|
removeSelected: (state, value) => {
|
||||||
let i = state.selected.indexOf(value)
|
let i = state.selected.indexOf(value)
|
||||||
if (i === -1) return
|
if (i === -1) return
|
||||||
@@ -35,8 +46,12 @@ const mutations = {
|
|||||||
resetSelected: (state) => {
|
resetSelected: (state) => {
|
||||||
state.selected = []
|
state.selected = []
|
||||||
},
|
},
|
||||||
listingDisplay: (state, value) => {
|
updateUser: (state, value) => {
|
||||||
state.req.display = value
|
if (typeof value !== 'object') return
|
||||||
|
|
||||||
|
for (let field in value) {
|
||||||
|
state.user[field] = value[field]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateRequest: (state, value) => {
|
updateRequest: (state, value) => {
|
||||||
state.req = value
|
state.req = value
|
||||||
@@ -48,6 +63,12 @@ const mutations = {
|
|||||||
resetClipboard: (state) => {
|
resetClipboard: (state) => {
|
||||||
state.clipboard.key = ''
|
state.clipboard.key = ''
|
||||||
state.clipboard.items = []
|
state.clipboard.items = []
|
||||||
|
},
|
||||||
|
setSchedule: (state, value) => {
|
||||||
|
state.schedule = value
|
||||||
|
},
|
||||||
|
setProgress: (state, value) => {
|
||||||
|
state.progress = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,23 @@ import store from '@/store'
|
|||||||
|
|
||||||
const ssl = (window.location.protocol === 'https:')
|
const ssl = (window.location.protocol === 'https:')
|
||||||
|
|
||||||
function removePrefix (url) {
|
export function removePrefix (url) {
|
||||||
if (url.startsWith('/files')) {
|
if (url.startsWith('/files')) {
|
||||||
return url.slice(6)
|
url = url.slice(6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url === '') url = '/'
|
||||||
|
if (url[0] !== '/') url = '/' + url
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetch (url) {
|
export function fetch (url) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('GET', `${store.state.baseURL}/api/resource${url}`, true)
|
request.open('GET', `${store.state.baseURL}/api/resource${url}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
switch (request.status) {
|
switch (request.status) {
|
||||||
@@ -24,10 +26,7 @@ function fetch (url) {
|
|||||||
resolve(JSON.parse(request.responseText))
|
resolve(JSON.parse(request.responseText))
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
reject({
|
reject(new Error(request.status))
|
||||||
message: request.responseText,
|
|
||||||
status: request.status
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,13 +35,13 @@ function fetch (url) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function rm (url) {
|
export function remove (url) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('DELETE', `${store.state.baseURL}/api/resource${url}`, true)
|
request.open('DELETE', `${store.state.baseURL}/api/resource${url}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
@@ -57,34 +56,51 @@ function rm (url) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function post (url, content = '') {
|
export function post (url, content = '', overwrite = false, onupload) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('POST', `${store.state.baseURL}/api/resource${url}`, true)
|
request.open('POST', `${store.state.baseURL}/api/resource${url}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
|
if (typeof onupload === 'function') {
|
||||||
|
request.upload.onprogress = onupload
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overwrite) {
|
||||||
|
request.setRequestHeader('Action', `override`)
|
||||||
|
}
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
resolve(request.responseText)
|
resolve(request.responseText)
|
||||||
|
} else if (request.status === 409) {
|
||||||
|
reject(request.status)
|
||||||
} else {
|
} else {
|
||||||
reject(request.responseText)
|
reject(request.responseText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onerror = (error) => reject(error)
|
request.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
request.send(content)
|
request.send(content)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function put (url, content = '') {
|
export function put (url, content = '', publish = false, date = '') {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true)
|
request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
request.setRequestHeader('Publish', publish)
|
||||||
|
|
||||||
|
if (date !== '') {
|
||||||
|
request.setRequestHeader('Schedule', date)
|
||||||
|
}
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
@@ -109,7 +125,7 @@ function moveCopy (items, copy = false) {
|
|||||||
promises.push(new Promise((resolve, reject) => {
|
promises.push(new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('PATCH', `${store.state.baseURL}/api/resource${from}`, true)
|
request.open('PATCH', `${store.state.baseURL}/api/resource${from}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
request.setRequestHeader('Destination', to)
|
request.setRequestHeader('Destination', to)
|
||||||
|
|
||||||
if (copy) {
|
if (copy) {
|
||||||
@@ -132,21 +148,21 @@ function moveCopy (items, copy = false) {
|
|||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
function move (items) {
|
export function move (items) {
|
||||||
return moveCopy(items)
|
return moveCopy(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
function copy (items) {
|
export function copy (items) {
|
||||||
return moveCopy(items, true)
|
return moveCopy(items, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function checksum (url, algo) {
|
export function checksum (url, algo) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('GET', `${store.state.baseURL}/api/checksum${url}?algo=${algo}`, true)
|
request.open('GET', `${store.state.baseURL}/api/checksum${url}?algo=${algo}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
@@ -160,7 +176,7 @@ function checksum (url, algo) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function command (url, command, onmessage, onclose) {
|
export function command (url, command, onmessage, onclose) {
|
||||||
let protocol = (ssl ? 'wss:' : 'ws:')
|
let protocol = (ssl ? 'wss:' : 'ws:')
|
||||||
url = removePrefix(url)
|
url = removePrefix(url)
|
||||||
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/command${url}`
|
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/command${url}`
|
||||||
@@ -171,7 +187,7 @@ function command (url, command, onmessage, onclose) {
|
|||||||
conn.onclose = onclose
|
conn.onclose = onclose
|
||||||
}
|
}
|
||||||
|
|
||||||
function search (url, search, onmessage, onclose) {
|
export function search (url, search, onmessage, onclose) {
|
||||||
let protocol = (ssl ? 'wss:' : 'ws:')
|
let protocol = (ssl ? 'wss:' : 'ws:')
|
||||||
url = removePrefix(url)
|
url = removePrefix(url)
|
||||||
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/search${url}`
|
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/search${url}`
|
||||||
@@ -182,7 +198,7 @@ function search (url, search, onmessage, onclose) {
|
|||||||
conn.onclose = onclose
|
conn.onclose = onclose
|
||||||
}
|
}
|
||||||
|
|
||||||
function download (format, ...files) {
|
export function download (format, ...files) {
|
||||||
let url = `${store.state.baseURL}/api/download`
|
let url = `${store.state.baseURL}/api/download`
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
@@ -206,11 +222,63 @@ function download (format, ...files) {
|
|||||||
window.open(url)
|
window.open(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUsers () {
|
export function getSettings () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let request = new window.XMLHttpRequest()
|
||||||
|
request.open('GET', `${store.state.baseURL}/api/settings/`, true)
|
||||||
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
|
request.onload = () => {
|
||||||
|
switch (request.status) {
|
||||||
|
case 200:
|
||||||
|
resolve(JSON.parse(request.responseText))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
reject(request.responseText)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.onerror = (error) => reject(error)
|
||||||
|
request.send()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSettings (param, which) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let data = {
|
||||||
|
what: 'settings',
|
||||||
|
which: which,
|
||||||
|
data: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.data[which] = param
|
||||||
|
|
||||||
|
let request = new window.XMLHttpRequest()
|
||||||
|
request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
|
||||||
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
|
request.onload = () => {
|
||||||
|
switch (request.status) {
|
||||||
|
case 200:
|
||||||
|
resolve()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
reject(request.responseText)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.onerror = (error) => { reject(error) }
|
||||||
|
request.send(JSON.stringify(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// USERS
|
||||||
|
|
||||||
|
export function getUsers () {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('GET', `${store.state.baseURL}/api/users/`, true)
|
request.open('GET', `${store.state.baseURL}/api/users/`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
switch (request.status) {
|
switch (request.status) {
|
||||||
@@ -227,11 +295,11 @@ function getUsers () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUser (id) {
|
export function getUser (id) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
|
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
switch (request.status) {
|
switch (request.status) {
|
||||||
@@ -248,11 +316,11 @@ function getUser (id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function newUser (user) {
|
export function newUser (user) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('POST', `${store.state.baseURL}/api/users/`, true)
|
request.open('POST', `${store.state.baseURL}/api/users/`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
switch (request.status) {
|
switch (request.status) {
|
||||||
@@ -265,15 +333,19 @@ function newUser (user) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.onerror = (error) => reject(error)
|
request.onerror = (error) => reject(error)
|
||||||
request.send(JSON.stringify(user))
|
request.send(JSON.stringify({
|
||||||
|
what: 'user',
|
||||||
|
which: 'new',
|
||||||
|
data: user
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUser (user) {
|
export function updateUser (user, which) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
|
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
switch (request.status) {
|
switch (request.status) {
|
||||||
@@ -286,15 +358,19 @@ function updateUser (user) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.onerror = (error) => reject(error)
|
request.onerror = (error) => reject(error)
|
||||||
request.send(JSON.stringify(user))
|
request.send(JSON.stringify({
|
||||||
|
what: 'user',
|
||||||
|
which: (typeof which === 'string') ? which : 'all',
|
||||||
|
data: user
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteUser (id) {
|
export function deleteUser (id) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
|
request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
switch (request.status) {
|
switch (request.status) {
|
||||||
@@ -311,153 +387,69 @@ function deleteUser (id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePassword (password) {
|
// SHARE
|
||||||
|
|
||||||
|
export function getShare (url) {
|
||||||
|
url = removePrefix(url)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('PUT', `${store.state.baseURL}/api/users/change-password`, true)
|
request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
switch (request.status) {
|
if (request.status === 200) {
|
||||||
case 200:
|
resolve(JSON.parse(request.responseText))
|
||||||
resolve()
|
} else {
|
||||||
break
|
reject(request.status)
|
||||||
default:
|
|
||||||
reject(request.responseText)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.onerror = (error) => reject(error)
|
|
||||||
request.send(JSON.stringify({ 'password': password }))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCSS (css) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let request = new window.XMLHttpRequest()
|
|
||||||
request.open('PUT', `${store.state.baseURL}/api/users/change-css`, true)
|
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
|
||||||
|
|
||||||
request.onload = () => {
|
|
||||||
switch (request.status) {
|
|
||||||
case 200:
|
|
||||||
resolve()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
reject(request.responseText)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request.onerror = (error) => reject(error)
|
|
||||||
request.send(JSON.stringify({ 'css': css }))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommands () {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let request = new window.XMLHttpRequest()
|
|
||||||
request.open('GET', `${store.state.baseURL}/api/commands/`, true)
|
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
|
||||||
|
|
||||||
request.onload = () => {
|
|
||||||
switch (request.status) {
|
|
||||||
case 200:
|
|
||||||
resolve(JSON.parse(request.responseText))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
reject(request.responseText)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request.onerror = (error) => reject(error)
|
request.onerror = (error) => reject(error)
|
||||||
request.send()
|
request.send()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCommands (commands) {
|
export function deleteShare (hash) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('PUT', `${store.state.baseURL}/api/commands/`, true)
|
request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
switch (request.status) {
|
if (request.status === 200) {
|
||||||
case 200:
|
resolve()
|
||||||
resolve()
|
} else {
|
||||||
break
|
reject(request.status)
|
||||||
default:
|
|
||||||
reject(request.responseText)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.onerror = (error) => reject(error)
|
|
||||||
request.send(JSON.stringify(commands))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlugins () {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let request = new window.XMLHttpRequest()
|
|
||||||
request.open('GET', `${store.state.baseURL}/api/plugins/`, true)
|
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
|
||||||
|
|
||||||
request.onload = () => {
|
|
||||||
switch (request.status) {
|
|
||||||
case 200:
|
|
||||||
resolve(JSON.parse(request.responseText))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
reject(request.responseText)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request.onerror = (error) => reject(error)
|
request.onerror = (error) => reject(error)
|
||||||
request.send()
|
request.send()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePlugins (data) {
|
export function share (url, expires = '', unit = 'hours') {
|
||||||
|
url = removePrefix(url)
|
||||||
|
url = `${store.state.baseURL}/api/share${url}`
|
||||||
|
if (expires !== '') {
|
||||||
|
url += `?expires=${expires}&unit=${unit}`
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('PUT', `${store.state.baseURL}/api/plugins/`, true)
|
request.open('POST', url, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
switch (request.status) {
|
if (request.status === 200) {
|
||||||
case 200:
|
resolve(JSON.parse(request.responseText))
|
||||||
resolve()
|
} else {
|
||||||
break
|
reject(request.responseStatus)
|
||||||
default:
|
|
||||||
reject(request.responseText)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.onerror = (error) => reject(error)
|
request.onerror = (error) => reject(error)
|
||||||
request.send(JSON.stringify(data))
|
request.send()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
|
||||||
delete: rm,
|
|
||||||
fetch,
|
|
||||||
checksum,
|
|
||||||
move,
|
|
||||||
put,
|
|
||||||
copy,
|
|
||||||
post,
|
|
||||||
command,
|
|
||||||
search,
|
|
||||||
download,
|
|
||||||
getUser,
|
|
||||||
newUser,
|
|
||||||
updateUser,
|
|
||||||
getUsers,
|
|
||||||
updatePassword,
|
|
||||||
updateCSS,
|
|
||||||
getCommands,
|
|
||||||
updateCommands,
|
|
||||||
removePrefix,
|
|
||||||
getPlugins,
|
|
||||||
updatePlugins,
|
|
||||||
deleteUser
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ function loggedIn () {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('GET', `${store.state.baseURL}/api/auth/renew`, true)
|
request.open('GET', `${store.state.baseURL}/api/auth/renew`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`)
|
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`)
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
parseToken(request.responseText)
|
parseToken(request.responseText)
|
||||||
resolve()
|
resolve()
|
||||||
} else {
|
} else {
|
||||||
reject()
|
reject(new Error(request.responseText))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.onerror = () => reject()
|
request.onerror = () => reject(new Error('Could not finish the request'))
|
||||||
request.send()
|
request.send()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ function login (user, password) {
|
|||||||
reject(request.responseText)
|
reject(request.responseText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.onerror = () => reject()
|
request.onerror = () => reject(new Error('Could not finish the request'))
|
||||||
request.send(JSON.stringify(data))
|
request.send(JSON.stringify(data))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function loading (button) {
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
function done (button, success = true) {
|
function done (button) {
|
||||||
let el = document.querySelector(`#${button}-button > i`)
|
let el = document.querySelector(`#${button}-button > i`)
|
||||||
|
|
||||||
if (el === undefined || el === null) {
|
if (el === undefined || el === null) {
|
||||||
@@ -33,7 +33,34 @@ function done (button, success = true) {
|
|||||||
}, 100)
|
}, 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 {
|
export default {
|
||||||
loading,
|
loading,
|
||||||
done
|
done,
|
||||||
|
success
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div id="breadcrumbs">
|
<div id="breadcrumbs">
|
||||||
<router-link to="/files/">
|
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||||
<i class="material-icons">home</i>
|
<i class="material-icons">home</i>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error">
|
<div v-if="error">
|
||||||
<not-found v-if="error === 404"></not-found>
|
<not-found v-if="error.message === '404'"></not-found>
|
||||||
<forbidden v-else-if="error === 403"></forbidden>
|
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||||
<internal-error v-else></internal-error>
|
<internal-error v-else></internal-error>
|
||||||
</div>
|
</div>
|
||||||
<editor v-else-if="isEditor"></editor>
|
<editor v-else-if="isEditor"></editor>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<preview v-else-if="isPreview"></preview>
|
<preview v-else-if="isPreview"></preview>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h2 class="message">
|
<h2 class="message">
|
||||||
<span>Loading...</span>
|
<span>{{ $t('files.loading') }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,10 +30,10 @@
|
|||||||
import Forbidden from './errors/403'
|
import Forbidden from './errors/403'
|
||||||
import NotFound from './errors/404'
|
import NotFound from './errors/404'
|
||||||
import InternalError from './errors/500'
|
import InternalError from './errors/500'
|
||||||
import Preview from './Preview'
|
import Preview from '@/components/files/Preview'
|
||||||
import Listing from './Listing'
|
import Listing from '@/components/files/Listing'
|
||||||
import Editor from './Editor'
|
import Editor from '@/components/files/Editor'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import { mapGetters, mapState, mapMutations } from 'vuex'
|
import { mapGetters, mapState, mapMutations } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -116,20 +116,14 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
window.addEventListener('keydown', this.keyEvent)
|
window.addEventListener('keydown', this.keyEvent)
|
||||||
window.addEventListener('scroll', event => {
|
window.addEventListener('scroll', this.scroll)
|
||||||
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
|
|
||||||
|
|
||||||
let top = 112 - window.scrollY
|
|
||||||
|
|
||||||
if (top < 64) {
|
|
||||||
top = 64
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector('#listing.list .item.header').style.top = top + 'px'
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
window.removeEventListener('keydown', this.keyEvent)
|
window.removeEventListener('keydown', this.keyEvent)
|
||||||
|
window.removeEventListener('scroll', this.scroll)
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.$store.commit('updateRequest', {})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations([ 'setLoading' ]),
|
...mapMutations([ 'setLoading' ]),
|
||||||
@@ -149,25 +143,19 @@ export default {
|
|||||||
if (url[0] !== '/') url = '/' + url
|
if (url[0] !== '/') url = '/' + url
|
||||||
|
|
||||||
api.fetch(url)
|
api.fetch(url)
|
||||||
.then((req) => {
|
.then((req) => {
|
||||||
if (!url.endsWith('/') && req.url.endsWith('/')) {
|
if (!url.endsWith('/') && req.url.endsWith('/')) {
|
||||||
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
|
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit('updateRequest', req)
|
this.$store.commit('updateRequest', req)
|
||||||
document.title = req.name
|
document.title = req.name
|
||||||
this.setLoading(false)
|
this.setLoading(false)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.setLoading(false)
|
this.setLoading(false)
|
||||||
|
this.error = error
|
||||||
if (typeof error === 'object') {
|
})
|
||||||
this.error = error.status
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.error = error
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
// Esc!
|
// Esc!
|
||||||
@@ -217,11 +205,21 @@ export default {
|
|||||||
|
|
||||||
if (this.req.kind !== 'editor') {
|
if (this.req.kind !== 'editor') {
|
||||||
document.getElementById('download-button').click()
|
document.getElementById('download-button').click()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
scroll (event) {
|
||||||
|
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
|
||||||
|
|
||||||
|
let top = 112 - window.scrollY
|
||||||
|
|
||||||
|
if (top < 64) {
|
||||||
|
top = 64
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#listing.list .item.header').style.top = top + 'px'
|
||||||
|
},
|
||||||
openSidebar () {
|
openSidebar () {
|
||||||
this.$store.commit('showHover', 'sidebar')
|
this.$store.commit('showHover', 'sidebar')
|
||||||
},
|
},
|
||||||
43
assets/src/views/Layout.vue
Normal file
43
assets/src/views/Layout.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div id="progress">
|
||||||
|
<div v-bind:style="{ width: $store.state.progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<site-header></site-header>
|
||||||
|
<sidebar></sidebar>
|
||||||
|
<main>
|
||||||
|
<router-view @css="$emit('update:css')"></router-view>
|
||||||
|
</main>
|
||||||
|
<prompts></prompts>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Search from '@/components/Search'
|
||||||
|
import Sidebar from '@/components/Sidebar'
|
||||||
|
import Prompts from '@/components/prompts/Prompts'
|
||||||
|
import SiteHeader from '@/components/Header'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'layout',
|
||||||
|
components: {
|
||||||
|
Search,
|
||||||
|
Sidebar,
|
||||||
|
SiteHeader,
|
||||||
|
Prompts
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route': function () {
|
||||||
|
this.$store.commit('resetSelected')
|
||||||
|
this.$store.commit('multiple', false)
|
||||||
|
if (this.$store.state.show !== 'success') this.$store.commit('closeHovers')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.$emit('update:css')
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
this.$emit('clean:css')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
42
assets/src/views/Login.vue
Normal file
42
assets/src/views/Login.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div id="login">
|
||||||
|
<form @submit="submit">
|
||||||
|
<img src="../assets/logo.svg" alt="File Manager">
|
||||||
|
<h1>File Manager</h1>
|
||||||
|
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
|
||||||
|
<input type="text" v-model="username" :placeholder="$t('login.username')">
|
||||||
|
<input type="password" v-model="password" :placeholder="$t('login.password')">
|
||||||
|
<input type="submit" :value="$t('login.submit')">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import auth from '@/utils/auth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'login',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
wrong: false,
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit: function (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
let redirect = this.$route.query.redirect
|
||||||
|
if (redirect === '' || redirect === undefined || redirect === null) {
|
||||||
|
redirect = '/files/'
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.login(this.username, this.password)
|
||||||
|
.then(() => { this.$router.push({ path: redirect }) })
|
||||||
|
.catch(() => { this.wrong = true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
20
assets/src/views/Settings.vue
Normal file
20
assets/src/views/Settings.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<ul id="nav" v-if="user.admin">
|
||||||
|
<li :class="{ active: $route.path === '/settings/profile' }"><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
|
||||||
|
<li :class="{ active: $route.path === '/settings/global' }"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
|
||||||
|
<li :class="{ active: $route.path === '/settings/users' }"><router-link to="/settings/users">{{ $t('settings.userManagement') }}</router-link></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<router-view @css="$emit('css')"></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'settings',
|
||||||
|
computed: mapState([ 'user' ])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="message">
|
<h2 class="message">
|
||||||
<i class="material-icons">error</i>
|
<i class="material-icons">error</i>
|
||||||
<span>You're not welcome here.</span>
|
<span>{{ $t('errors.forbidden') }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="message">
|
<h2 class="message">
|
||||||
<i class="material-icons">gps_off</i>
|
<i class="material-icons">gps_off</i>
|
||||||
<span>This location can't be reached.</span>
|
<span>{{ $t('errors.notFound') }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="message">
|
<h2 class="message">
|
||||||
<i class="material-icons">error_outline</i>
|
<i class="material-icons">error_outline</i>
|
||||||
<span>Something really went wrong.</span>
|
<span>{{ $t('errors.internal') }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
188
assets/src/views/settings/Global.vue
Normal file
188
assets/src/views/settings/Global.vue
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<form class="card" @submit.prevent="saveStaticGen">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ capitalize($store.state.staticGen) }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p v-for="field in staticGen" :key="field.variable">
|
||||||
|
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
|
||||||
|
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
|
||||||
|
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
|
||||||
|
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="card" @submit.prevent="saveCSS">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('settings.customStylesheet') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<textarea v-model="css"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="card" @submit.prevent="saveCommands">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('settings.commands') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="small">{{ $t('settings.commandsHelp') }}</p>
|
||||||
|
|
||||||
|
<div v-for="command in commands" :key="command.name" class="collapsible">
|
||||||
|
<input :id="command.name" type="checkbox">
|
||||||
|
<label :for="command.name">
|
||||||
|
<p>{{ capitalize(command.name) }}</p>
|
||||||
|
<i class="material-icons">arrow_drop_down</i>
|
||||||
|
</label>
|
||||||
|
<div class="collapse">
|
||||||
|
<textarea v-model.trim="command.value"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import { getSettings, updateSettings } from '@/utils/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'settings',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
commands: [],
|
||||||
|
staticGen: [],
|
||||||
|
css: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState([ 'user' ])
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
getSettings()
|
||||||
|
.then(settings => {
|
||||||
|
console.log(settings)
|
||||||
|
if (this.$store.state.staticGen.length > 0) {
|
||||||
|
this.parseStaticGen(settings.staticGen)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in settings.commands) {
|
||||||
|
this.commands.push({
|
||||||
|
name: key,
|
||||||
|
value: settings.commands[key].join('\n')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.css = settings.css
|
||||||
|
})
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
capitalize (name, where = '_') {
|
||||||
|
if (where === 'caps') where = /(?=[A-Z])/
|
||||||
|
let splitted = name.split(where)
|
||||||
|
name = ''
|
||||||
|
|
||||||
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
|
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.slice(0, -1)
|
||||||
|
},
|
||||||
|
saveCommands (event) {
|
||||||
|
let commands = {}
|
||||||
|
|
||||||
|
for (let command of this.commands) {
|
||||||
|
let value = command.value.split('\n')
|
||||||
|
if (value.length === 1 && value[0] === '') {
|
||||||
|
value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
commands[command.name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSettings(commands, 'commands')
|
||||||
|
.then(() => { this.$showSuccess(this.$t('settings.commandsUpdated')) })
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
saveCSS (event) {
|
||||||
|
updateSettings(this.css, 'css')
|
||||||
|
.then(() => {
|
||||||
|
this.$showSuccess(this.$t('settings.settingsUpdated'))
|
||||||
|
this.$store.commit('setCSS', this.css)
|
||||||
|
this.$emit('css')
|
||||||
|
})
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
saveStaticGen (event) {
|
||||||
|
let staticGen = {}
|
||||||
|
|
||||||
|
for (let field of this.staticGen) {
|
||||||
|
staticGen[field.variable] = field.value
|
||||||
|
|
||||||
|
if (field.original === 'array') {
|
||||||
|
let val = field.value.split(' ')
|
||||||
|
if (val[0] === '') {
|
||||||
|
val.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
staticGen[field.variable] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSettings(staticGen, 'staticGen')
|
||||||
|
.then(() => { this.$showSuccess(this.$t('settings.settingsUpdated')) })
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
parseStaticGen (staticgen) {
|
||||||
|
for (let option of staticgen) {
|
||||||
|
let value = option.value
|
||||||
|
|
||||||
|
let field = {
|
||||||
|
name: option.name,
|
||||||
|
variable: option.variable,
|
||||||
|
type: 'text',
|
||||||
|
original: 'text',
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
field.original = 'array'
|
||||||
|
field.value = value.join(' ')
|
||||||
|
|
||||||
|
this.staticGen.push(field)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'boolean':
|
||||||
|
field.type = 'checkbox'
|
||||||
|
field.original = 'boolean'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
this.staticGen.push(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
109
assets/src/views/settings/Profile.vue
Normal file
109
assets/src/views/settings/Profile.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<form class="card" @submit="updateSettings">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('settings.profileSettings') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<h3>{{ $t('settings.language') }}</h3>
|
||||||
|
<p><languages id="locale" :selected.sync="locale"></languages></p>
|
||||||
|
<h3>{{ $t('settings.customStylesheet') }}</h3>
|
||||||
|
<textarea v-model="css" name="css"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="card" v-if="!user.lockPassword" @submit="updatePassword">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('settings.changePassword') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p>
|
||||||
|
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import { updateUser } from '@/utils/api'
|
||||||
|
import Languages from '@/components/Languages'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'settings',
|
||||||
|
components: {
|
||||||
|
Languages
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
password: '',
|
||||||
|
passwordConf: '',
|
||||||
|
css: '',
|
||||||
|
locale: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState([ 'user' ]),
|
||||||
|
passwordClass () {
|
||||||
|
if (this.password === '' && this.passwordConf === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.password === this.passwordConf) {
|
||||||
|
return 'green'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'red'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.css = this.user.css
|
||||||
|
this.locale = this.user.locale
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updatePassword (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (this.password !== this.passwordConf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = {
|
||||||
|
ID: this.$store.state.user.ID,
|
||||||
|
password: this.password
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser(user, 'password').then(location => {
|
||||||
|
this.$showSuccess(this.$t('settings.passwordUpdated'))
|
||||||
|
}).catch(e => {
|
||||||
|
this.$showError(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSettings (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
let user = {...this.$store.state.user}
|
||||||
|
user.css = this.css
|
||||||
|
user.locale = this.locale
|
||||||
|
|
||||||
|
updateUser(user, 'partial').then(location => {
|
||||||
|
this.$store.commit('setUser', user)
|
||||||
|
this.$emit('css')
|
||||||
|
this.$showSuccess(this.$t('settings.settingsUpdated'))
|
||||||
|
}).catch(e => {
|
||||||
|
this.$showError(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
305
assets/src/views/settings/User.vue
Normal file
305
assets/src/views/settings/User.vue
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<form @submit="save" class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2 v-if="id === 0">{{ $t('settings.newUser') }}</h2>
|
||||||
|
<h2 v-else>{{ $t('settings.user') }} {{ username }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></p>
|
||||||
|
<p><label for="password">{{ $t('settings.password') }}</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
|
||||||
|
<p><label for="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p>
|
||||||
|
<p>
|
||||||
|
<label for="locale">{{ $t('settings.language') }}</label>
|
||||||
|
<languages id="locale" :selected.sync="locale"></languages>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><input type="checkbox" :disabled="admin" v-model="lockPassword"> {{ $t('settings.lockPassword') }}</p>
|
||||||
|
|
||||||
|
<h3>{{ $t('settings.permissions') }}</h3>
|
||||||
|
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
|
||||||
|
|
||||||
|
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
|
||||||
|
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
|
||||||
|
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
|
||||||
|
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
|
||||||
|
<p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
|
||||||
|
|
||||||
|
<h3>{{ $t('settings.userCommands') }}</h3>
|
||||||
|
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
|
||||||
|
<input type="text" v-model.trim="commands">
|
||||||
|
|
||||||
|
<h3>{{ $t('settings.rules') }}</h3>
|
||||||
|
|
||||||
|
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
|
||||||
|
|
||||||
|
<i18n path="settings.rulesHelp2" tag="p" class="small">
|
||||||
|
<code>allow</code><code>disallow</code><code>regex</code>
|
||||||
|
</i18n>
|
||||||
|
|
||||||
|
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
|
||||||
|
|
||||||
|
<ul class="small">
|
||||||
|
<li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
|
||||||
|
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<textarea v-model.trim="rules"></textarea>
|
||||||
|
|
||||||
|
<h3>{{ $t('settings.customStylesheet') }}</h3>
|
||||||
|
|
||||||
|
<textarea name="css"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="flat delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||||
|
<input class="flat" type="submit" :value="$t('buttons.save')">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div v-if="$store.state.show === 'deleteUser'" class="card floating">
|
||||||
|
<div class="card-content">
|
||||||
|
<p>Are you sure you want to delete this user?</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="cancel flat"
|
||||||
|
@click="closeHovers"
|
||||||
|
autofocus
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">
|
||||||
|
{{ $t('buttons.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button class="flat"
|
||||||
|
@click="deleteUser">
|
||||||
|
{{ $t('buttons.delete') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapMutations } from 'vuex'
|
||||||
|
import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
|
||||||
|
import Languages from '@/components/Languages'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'user',
|
||||||
|
components: { Languages },
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
id: 0,
|
||||||
|
admin: false,
|
||||||
|
allowNew: false,
|
||||||
|
allowEdit: false,
|
||||||
|
allowCommands: false,
|
||||||
|
allowPublish: false,
|
||||||
|
lockPassword: false,
|
||||||
|
permissions: {},
|
||||||
|
password: '',
|
||||||
|
username: '',
|
||||||
|
filesystem: '',
|
||||||
|
rules: '',
|
||||||
|
locale: '',
|
||||||
|
css: '',
|
||||||
|
commands: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
passwordPlaceholder () {
|
||||||
|
if (this.$route.path === '/settings/users/new') return ''
|
||||||
|
return this.$t('settings.avoidChanges')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route': 'fetchData',
|
||||||
|
admin: function () {
|
||||||
|
if (!this.admin) return
|
||||||
|
this.allowCommands = true
|
||||||
|
this.allowEdit = true
|
||||||
|
this.allowNew = true
|
||||||
|
this.allowPublish = true
|
||||||
|
this.lockPassword = false
|
||||||
|
for (let key in this.permissions) {
|
||||||
|
this.permissions[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations(['closeHovers']),
|
||||||
|
fetchData () {
|
||||||
|
let user = this.$route.params[0]
|
||||||
|
|
||||||
|
if (this.$route.path === '/settings/users/new') {
|
||||||
|
user = 'base'
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser(user).then(user => {
|
||||||
|
this.id = user.ID
|
||||||
|
this.admin = user.admin
|
||||||
|
this.allowCommands = user.allowCommands
|
||||||
|
this.allowNew = user.allowNew
|
||||||
|
this.allowEdit = user.allowEdit
|
||||||
|
this.allowPublish = user.allowPublish
|
||||||
|
this.lockPassword = user.lockPassword
|
||||||
|
this.filesystem = user.filesystem
|
||||||
|
this.username = user.username
|
||||||
|
this.commands = user.commands.join(' ')
|
||||||
|
this.css = user.css
|
||||||
|
this.permissions = user.permissions
|
||||||
|
this.locale = user.locale
|
||||||
|
|
||||||
|
for (let rule of user.rules) {
|
||||||
|
if (rule.allow) {
|
||||||
|
this.rules += 'allow '
|
||||||
|
} else {
|
||||||
|
this.rules += 'disallow '
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.regex) {
|
||||||
|
this.rules += 'regex ' + rule.regexp.raw
|
||||||
|
} else {
|
||||||
|
this.rules += rule.path
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rules += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rules = this.rules.trim()
|
||||||
|
}).catch(() => {
|
||||||
|
this.$router.push({ path: '/settings/users/new' })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
capitalize (name) {
|
||||||
|
let splitted = name.split(/(?=[A-Z])/)
|
||||||
|
name = ''
|
||||||
|
|
||||||
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
|
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.slice(0, -1)
|
||||||
|
},
|
||||||
|
reset () {
|
||||||
|
this.id = 0
|
||||||
|
this.admin = false
|
||||||
|
this.allowNew = false
|
||||||
|
this.allowEdit = false
|
||||||
|
this.allowPublish = false
|
||||||
|
this.permissins = {}
|
||||||
|
this.allowCommands = false
|
||||||
|
this.lockPassword = false
|
||||||
|
this.password = ''
|
||||||
|
this.username = ''
|
||||||
|
this.filesystem = ''
|
||||||
|
this.rules = ''
|
||||||
|
this.locale = ''
|
||||||
|
this.css = ''
|
||||||
|
this.commands = ''
|
||||||
|
},
|
||||||
|
deletePrompt (event) {
|
||||||
|
this.$store.commit('showHover', 'deleteUser')
|
||||||
|
},
|
||||||
|
deleteUser (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
deleteUser(this.id).then(location => {
|
||||||
|
this.$router.push({ path: '/settings/users' })
|
||||||
|
this.$showSuccess(this.$t('settings.userDeleted'))
|
||||||
|
}).catch(e => {
|
||||||
|
this.$showError(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
save (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
let user = this.parseForm()
|
||||||
|
|
||||||
|
if (this.$route.path === '/settings/users/new') {
|
||||||
|
newUser(user).then(location => {
|
||||||
|
this.$router.push({ path: location })
|
||||||
|
this.$showSuccess(this.$t('settings.userCreated'))
|
||||||
|
}).catch(e => {
|
||||||
|
this.$showError(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser(user).then(location => {
|
||||||
|
if (user.ID === this.$store.state.user.ID) {
|
||||||
|
this.$store.commit('setUser', user)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$showSuccess(this.$t('settings.userUpdated'))
|
||||||
|
}).catch(e => {
|
||||||
|
this.$showError(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
parseForm () {
|
||||||
|
let user = {
|
||||||
|
ID: this.id,
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
lockPassword: this.lockPassword,
|
||||||
|
filesystem: this.filesystem,
|
||||||
|
admin: this.admin,
|
||||||
|
allowCommands: this.allowCommands,
|
||||||
|
allowNew: this.allowNew,
|
||||||
|
allowEdit: this.allowEdit,
|
||||||
|
allowPublish: this.allowPublish,
|
||||||
|
permissions: this.permissions,
|
||||||
|
css: this.css,
|
||||||
|
locale: this.locale,
|
||||||
|
commands: this.commands.split(' '),
|
||||||
|
rules: []
|
||||||
|
}
|
||||||
|
|
||||||
|
let rules = this.rules.split('\n')
|
||||||
|
|
||||||
|
for (let rawRule of rules) {
|
||||||
|
let rule = {
|
||||||
|
allow: true,
|
||||||
|
path: '',
|
||||||
|
regex: false,
|
||||||
|
regexp: {
|
||||||
|
raw: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rawRule = rawRule.split(' ')
|
||||||
|
|
||||||
|
// Skip a malformed rule
|
||||||
|
if (rawRule.length < 2) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip a malformed rule
|
||||||
|
if (rawRule[0] !== 'allow' && rawRule[0] !== 'disallow') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.allow = (rawRule[0] === 'allow')
|
||||||
|
rawRule.shift()
|
||||||
|
|
||||||
|
if (rawRule[0] === 'regex') {
|
||||||
|
rule.regex = true
|
||||||
|
rawRule.shift()
|
||||||
|
rule.regexp.raw = rawRule.join(' ')
|
||||||
|
} else {
|
||||||
|
rule.path = rawRule.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
user.rules.push(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
48
assets/src/views/settings/Users.vue
Normal file
48
assets/src/views/settings/Users.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('settings.users') }}</h2>
|
||||||
|
<router-link to="/settings/users/new"><button class="flat">{{ $t('buttons.new') }}</button></router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content full">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('settings.username') }}</th>
|
||||||
|
<th>{{ $t('settings.admin') }}</th>
|
||||||
|
<th>{{ $t('settings.scope') }}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-for="user in users" :key="user.id">
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
|
||||||
|
<td>{{ user.filesystem }}</td>
|
||||||
|
<td class="small">
|
||||||
|
<router-link :to="'/settings/users/' + user.ID"><i class="material-icons">mode_edit</i></router-link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'users',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
api.getUsers().then(users => {
|
||||||
|
this.users = users
|
||||||
|
}).catch(error => {
|
||||||
|
this.$showError(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
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>
|
||||||
92
auth_test.go
92
auth_test.go
@@ -1,92 +0,0 @@
|
|||||||
package filemanager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultCredentials = "{\"username\":\"admin\",\"password\":\"admin\"}"
|
|
||||||
|
|
||||||
var authHandlerTests = []struct {
|
|
||||||
Data string
|
|
||||||
Expected int
|
|
||||||
}{
|
|
||||||
{defaultCredentials, http.StatusOK},
|
|
||||||
{"{\"username\":\"admin\",\"password\":\"wrong\"}", http.StatusForbidden},
|
|
||||||
{"{\"username\":\"wrong\",\"password\":\"admin\"}", http.StatusForbidden},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthHandler(t *testing.T) {
|
|
||||||
fm := newTest(t)
|
|
||||||
defer fm.Clean()
|
|
||||||
|
|
||||||
for _, test := range authHandlerTests {
|
|
||||||
req, err := http.NewRequest("POST", "/api/auth/get", strings.NewReader(test.Data))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
fm.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Code != test.Expected {
|
|
||||||
t.Errorf("Wrong status code: got %v want %v", w.Code, test.Expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenewHandler(t *testing.T) {
|
|
||||||
fm := newTest(t)
|
|
||||||
defer fm.Clean()
|
|
||||||
|
|
||||||
// First, we have to make an auth request to get the user authenticated,
|
|
||||||
r, err := http.NewRequest("POST", "/api/auth/get", strings.NewReader(defaultCredentials))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
fm.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
|
||||||
t.Errorf("Couldn't authenticate: got %v", w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
token := w.Body.String()
|
|
||||||
|
|
||||||
// Test renew authorization via Authorization Header.
|
|
||||||
r, err = http.NewRequest("GET", "/api/auth/renew", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
fm.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
|
||||||
t.Errorf("Can't renew auth via header: got %v", w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test renew authorization via cookie field.
|
|
||||||
r, err = http.NewRequest("GET", "/api/auth/renew", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.AddCookie(&http.Cookie{
|
|
||||||
Value: token,
|
|
||||||
Name: "auth",
|
|
||||||
Expires: time.Now().Add(1 * time.Hour),
|
|
||||||
})
|
|
||||||
|
|
||||||
w = httptest.NewRecorder()
|
|
||||||
fm.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
if w.Code != http.StatusOK {
|
|
||||||
t.Errorf("Can't renew auth via cookie: got %v", w.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
bolt/config.go
Normal file
26
bolt/config.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/asdine/storm"
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigStore is a configuration store.
|
||||||
|
type ConfigStore struct {
|
||||||
|
DB *storm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a configuration from the database to an interface.
|
||||||
|
func (c ConfigStore) Get(name string, to interface{}) error {
|
||||||
|
err := c.DB.Get("config", name, to)
|
||||||
|
if err == storm.ErrNotFound {
|
||||||
|
return fm.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves a configuration from an interface to the database.
|
||||||
|
func (c ConfigStore) Save(name string, from interface{}) error {
|
||||||
|
return c.DB.Set("config", name, from)
|
||||||
|
}
|
||||||
66
bolt/share.go
Normal file
66
bolt/share.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/asdine/storm"
|
||||||
|
"github.com/asdine/storm/q"
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShareStore is a shareable links store.
|
||||||
|
type ShareStore struct {
|
||||||
|
DB *storm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a Share Link from an hash.
|
||||||
|
func (s ShareStore) Get(hash string) (*fm.ShareLink, error) {
|
||||||
|
var v fm.ShareLink
|
||||||
|
err := s.DB.One("Hash", hash, &v)
|
||||||
|
if err == storm.ErrNotFound {
|
||||||
|
return nil, fm.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return &v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPermanent gets the permanent link from a path.
|
||||||
|
func (s ShareStore) GetPermanent(path string) (*fm.ShareLink, error) {
|
||||||
|
var v fm.ShareLink
|
||||||
|
err := s.DB.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&v)
|
||||||
|
if err == storm.ErrNotFound {
|
||||||
|
return nil, fm.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return &v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByPath gets all the links for a specific path.
|
||||||
|
func (s ShareStore) GetByPath(hash string) ([]*fm.ShareLink, error) {
|
||||||
|
var v []*fm.ShareLink
|
||||||
|
err := s.DB.Find("Path", hash, &v)
|
||||||
|
if err == storm.ErrNotFound {
|
||||||
|
return v, fm.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets retrieves all the shareable links.
|
||||||
|
func (s ShareStore) Gets() ([]*fm.ShareLink, error) {
|
||||||
|
var v []*fm.ShareLink
|
||||||
|
err := s.DB.All(&v)
|
||||||
|
if err == storm.ErrNotFound {
|
||||||
|
return v, fm.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save stores a Share Link on the database.
|
||||||
|
func (s ShareStore) Save(l *fm.ShareLink) error {
|
||||||
|
return s.DB.Save(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a Share Link from the database.
|
||||||
|
func (s ShareStore) Delete(hash string) error {
|
||||||
|
return s.DB.DeleteStruct(&fm.ShareLink{Hash: hash})
|
||||||
|
}
|
||||||
90
bolt/users.go
Normal file
90
bolt/users.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/asdine/storm"
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UsersStore is a users store.
|
||||||
|
type UsersStore struct {
|
||||||
|
DB *storm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a user with a certain id from the database.
|
||||||
|
func (u UsersStore) Get(id int, builder fm.FSBuilder) (*fm.User, error) {
|
||||||
|
var us fm.User
|
||||||
|
err := u.DB.One("ID", id, &us)
|
||||||
|
if err == storm.ErrNotFound {
|
||||||
|
return nil, fm.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
us.FileSystem = builder(us.Scope)
|
||||||
|
return &us, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUsername gets a user with a certain username from the database.
|
||||||
|
func (u UsersStore) GetByUsername(username string, builder fm.FSBuilder) (*fm.User, error) {
|
||||||
|
var us fm.User
|
||||||
|
err := u.DB.One("Username", username, &us)
|
||||||
|
if err == storm.ErrNotFound {
|
||||||
|
return nil, fm.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
us.FileSystem = builder(us.Scope)
|
||||||
|
return &us, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets gets all the users from the database.
|
||||||
|
func (u UsersStore) Gets(builder fm.FSBuilder) ([]*fm.User, error) {
|
||||||
|
var us []*fm.User
|
||||||
|
err := u.DB.All(&us)
|
||||||
|
if err == storm.ErrNotFound {
|
||||||
|
return nil, fm.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return us, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range us {
|
||||||
|
user.FileSystem = builder(user.Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
return us, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the whole user object or only certain fields.
|
||||||
|
func (u UsersStore) Update(us *fm.User, fields ...string) error {
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return u.Save(us)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
val := reflect.ValueOf(us).Elem().FieldByName(field).Interface()
|
||||||
|
if err := u.DB.UpdateField(us, field, val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves a user to the database.
|
||||||
|
func (u UsersStore) Save(us *fm.User) error {
|
||||||
|
return u.DB.Save(us)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a user from the database.
|
||||||
|
func (u UsersStore) Delete(id int) error {
|
||||||
|
return u.DB.DeleteStruct(&fm.User{ID: id})
|
||||||
|
}
|
||||||
@@ -4,16 +4,11 @@
|
|||||||
package filemanager
|
package filemanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
. "github.com/hacdias/filemanager"
|
"github.com/hacdias/filemanager"
|
||||||
"github.com/hacdias/fileutils"
|
"github.com/hacdias/filemanager/caddy/parser"
|
||||||
|
h "github.com/hacdias/filemanager/http"
|
||||||
"github.com/mholt/caddy"
|
"github.com/mholt/caddy"
|
||||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
)
|
)
|
||||||
@@ -27,23 +22,18 @@ func init() {
|
|||||||
|
|
||||||
type plugin struct {
|
type plugin struct {
|
||||||
Next httpserver.Handler
|
Next httpserver.Handler
|
||||||
Configs []*config
|
Configs []*filemanager.FileManager
|
||||||
}
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
*FileManager
|
|
||||||
baseURL string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||||
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
for i := range f.Configs {
|
for i := range f.Configs {
|
||||||
// Checks if this Path should be handled by File Manager.
|
// Checks if this Path should be handled by File Manager.
|
||||||
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].baseURL) {
|
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Configs[i].ServeHTTP(w, r)
|
h.Handler(f.Configs[i]).ServeHTTP(w, r)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +42,7 @@ func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
|||||||
|
|
||||||
// setup configures a new FileManager middleware instance.
|
// setup configures a new FileManager middleware instance.
|
||||||
func setup(c *caddy.Controller) error {
|
func setup(c *caddy.Controller) error {
|
||||||
configs, err := parse(c)
|
configs, err := parser.Parse(c, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -63,93 +53,3 @@ func setup(c *caddy.Controller) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parse(c *caddy.Controller) ([]*config, error) {
|
|
||||||
var (
|
|
||||||
configs []*config
|
|
||||||
)
|
|
||||||
|
|
||||||
for c.Next() {
|
|
||||||
baseURL := "/"
|
|
||||||
baseScope := "."
|
|
||||||
database := ""
|
|
||||||
|
|
||||||
// Get the baseURL and baseScope
|
|
||||||
args := c.RemainingArgs()
|
|
||||||
|
|
||||||
if len(args) >= 1 {
|
|
||||||
baseURL = args[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) > 1 {
|
|
||||||
baseScope = args[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for c.NextBlock() {
|
|
||||||
switch c.Val() {
|
|
||||||
case "database":
|
|
||||||
if !c.NextArg() {
|
|
||||||
return nil, c.ArgErr()
|
|
||||||
}
|
|
||||||
|
|
||||||
database = c.Val()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
caddyConf := httpserver.GetConfig(c)
|
|
||||||
|
|
||||||
path := filepath.Join(caddy.AssetsPath(), "filemanager")
|
|
||||||
err := os.MkdirAll(path, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a database path and it is not absolute,
|
|
||||||
// it will be relative to Caddy folder.
|
|
||||||
if !filepath.IsAbs(database) && database != "" {
|
|
||||||
database = filepath.Join(path, database)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no database path on the settings,
|
|
||||||
// store one in .caddy/filemanager/name.db.
|
|
||||||
if database == "" {
|
|
||||||
// The name of the database is the hashed value of a string composed
|
|
||||||
// by the host, address path and the baseurl of this File Manager
|
|
||||||
// instance.
|
|
||||||
hasher := md5.New()
|
|
||||||
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL))
|
|
||||||
sha := hex.EncodeToString(hasher.Sum(nil))
|
|
||||||
database = filepath.Join(path, sha+".db")
|
|
||||||
|
|
||||||
fmt.Println("[WARNING] A database is going to be created for your File Manager instace at " + database +
|
|
||||||
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fm, err := New(database, User{
|
|
||||||
AllowCommands: true,
|
|
||||||
AllowEdit: true,
|
|
||||||
AllowNew: true,
|
|
||||||
Commands: []string{"git", "svn", "hg"},
|
|
||||||
Rules: []*Rule{{
|
|
||||||
Regex: true,
|
|
||||||
Allow: false,
|
|
||||||
Regexp: &Regexp{Raw: "\\/\\..+"},
|
|
||||||
}},
|
|
||||||
CSS: "",
|
|
||||||
FileSystem: fileutils.Dir(baseScope),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &config{FileManager: fm}
|
|
||||||
m.SetBaseURL(baseURL)
|
|
||||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
|
||||||
m.baseURL = strings.TrimSuffix(baseURL, "/")
|
|
||||||
|
|
||||||
configs = append(configs, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
return configs, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
# hugo - a caddy plugin
|
|
||||||
|
|
||||||
[](https://caddy.community)
|
|
||||||
|
|
||||||
hugo fills the gap between Hugo and the browser. [Hugo][6] is an easy and fast static website generator. This plugin fills the gap between Hugo and the end-user, providing you a web interface to manage the whole website.
|
|
||||||
|
|
||||||
Using this plugin, you won't need to have your own computer to edit posts, neither regenerate your static website, because you can do all of that just through your browser. It is an implementation of [hacdias/filemanager][1] library.
|
|
||||||
|
|
||||||
**Requirements:** you need to have the hugo executable in your PATH. You can download it from its [official page][6].
|
|
||||||
|
|
||||||
## Get Started
|
|
||||||
|
|
||||||
To start using this plugin you just need to go to the [download Caddy page][3] and choose `http.hugo` in the directives section. For further information on how Caddy works refer to [its documentation][4].
|
|
||||||
|
|
||||||
The default credentials are `admin` for both the user and the password. It is highy recommended to change them after logging in for the first time and to use HTTPS. You can create more users and define their own permissions using the web interface.
|
|
||||||
|
|
||||||
## Syntax
|
|
||||||
|
|
||||||
```
|
|
||||||
hugo [directory] [admin] {
|
|
||||||
database path
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
+ `directory` is the path, relative or absolute to the directory of your Hugo files. Defaults to `./`.
|
|
||||||
+ `admin` is the URL path where you will access the admin interface. Defaults to `/admin`.
|
|
||||||
+ `path` is the database path where the settings will be stored. By default, the settings will be stored on [`.caddy`][5] folder.
|
|
||||||
|
|
||||||
## Database
|
|
||||||
|
|
||||||
By default the database will be stored on [`.caddy`][5] directory, in a sub-directory called `hugo`. Each file name is an hash of the combination of the host and the base URL.
|
|
||||||
|
|
||||||
If you don't set a database path, you will receive a warning like this:
|
|
||||||
|
|
||||||
> [WARNING] A database is going to be created for your File Manager instace at ~/.caddy/hugo/xxx.db. It is highly recommended that you set the 'database' option to 'xxx.db'
|
|
||||||
|
|
||||||
Why? If you don't set a database path and you change the host or the base URL, your settings will be reseted. So it is *highly* recommended to set this option.
|
|
||||||
|
|
||||||
When you set a relative path, such as `xxxxxxxxxx.db`, it will always be relative to `.caddy/hugo` directory. Although, you may also use an absolute path if you wish to store the database in other place.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Manage the current working directory's Hugo website at `/admin` and display the ```public``` folder to the user.
|
|
||||||
|
|
||||||
```
|
|
||||||
root public
|
|
||||||
hugo {
|
|
||||||
database myinstance.db
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Manage the Hugo website located at `/var/www/mysite` at `/admin` and display the ```public``` folder to the user.
|
|
||||||
|
|
||||||
```
|
|
||||||
root /var/www/mysite/public
|
|
||||||
hugo /var/www/mysite {
|
|
||||||
database myinstance.db
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Manage the Hugo website located at `/var/www/mysite` at `/private` and display the ```public``` folder to the user.
|
|
||||||
|
|
||||||
```
|
|
||||||
root /var/www/mysite/public
|
|
||||||
hugo /var/www/mysite /private {
|
|
||||||
database myinstance.db
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Known Issues
|
|
||||||
|
|
||||||
If you are having troubles **handling large files** you might need to check out the [`timeouts`][2] plugin, which can be used to change the default HTTP Timeouts.
|
|
||||||
|
|
||||||
[1]:https://github.com/hacdias/filemanager
|
|
||||||
[2]:https://caddyserver.com/docs/timeouts
|
|
||||||
[3]:https://caddyserver.com/download
|
|
||||||
[4]:https://caddyserver.com/docs
|
|
||||||
[5]:https://caddyserver.com/docs/automatic-https#dot-caddy
|
|
||||||
[6]:http://gohugo.io
|
|
||||||
@@ -1,160 +1,15 @@
|
|||||||
package hugo
|
package hugo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hacdias/filemanager"
|
"github.com/hacdias/filemanager"
|
||||||
"github.com/hacdias/filemanager/plugins"
|
"github.com/hacdias/filemanager/caddy/parser"
|
||||||
"github.com/hacdias/fileutils"
|
h "github.com/hacdias/filemanager/http"
|
||||||
"github.com/mholt/caddy"
|
"github.com/mholt/caddy"
|
||||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setup configures a new FileManager middleware instance.
|
|
||||||
func setup(c *caddy.Controller) error {
|
|
||||||
configs, err := parse(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
|
||||||
return plugin{Configs: configs, Next: next}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
|
||||||
var (
|
|
||||||
configs []*filemanager.FileManager
|
|
||||||
)
|
|
||||||
|
|
||||||
for c.Next() {
|
|
||||||
// hugo [directory] [admin] {
|
|
||||||
// database path
|
|
||||||
// }
|
|
||||||
directory := "."
|
|
||||||
admin := "/admin"
|
|
||||||
database := ""
|
|
||||||
|
|
||||||
// Get the baseURL and baseScope
|
|
||||||
args := c.RemainingArgs()
|
|
||||||
|
|
||||||
if len(args) >= 1 {
|
|
||||||
directory = args[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) > 1 {
|
|
||||||
admin = args[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for c.NextBlock() {
|
|
||||||
switch c.Val() {
|
|
||||||
case "database":
|
|
||||||
if !c.NextArg() {
|
|
||||||
return nil, c.ArgErr()
|
|
||||||
}
|
|
||||||
|
|
||||||
database = c.Val()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
caddyConf := httpserver.GetConfig(c)
|
|
||||||
|
|
||||||
path := filepath.Join(caddy.AssetsPath(), "hugo")
|
|
||||||
err := os.MkdirAll(path, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a database path and it is not absolute,
|
|
||||||
// it will be relative to ".caddy" folder.
|
|
||||||
if !filepath.IsAbs(database) && database != "" {
|
|
||||||
database = filepath.Join(path, database)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no database path on the settings,
|
|
||||||
// store one in .caddy/hugo/{name}.db.
|
|
||||||
if database == "" {
|
|
||||||
// The name of the database is the hashed value of a string composed
|
|
||||||
// by the host, address path and the baseurl of this File Manager
|
|
||||||
// instance.
|
|
||||||
hasher := md5.New()
|
|
||||||
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + admin))
|
|
||||||
sha := hex.EncodeToString(hasher.Sum(nil))
|
|
||||||
database = filepath.Join(path, sha+".db")
|
|
||||||
|
|
||||||
fmt.Println("[WARNING] A database is going to be created for your Hugo instace at " + database +
|
|
||||||
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := filemanager.New(database, filemanager.User{
|
|
||||||
AllowCommands: true,
|
|
||||||
AllowEdit: true,
|
|
||||||
AllowNew: true,
|
|
||||||
Permissions: map[string]bool{},
|
|
||||||
Commands: []string{"git", "svn", "hg"},
|
|
||||||
Rules: []*filemanager.Rule{{
|
|
||||||
Regex: true,
|
|
||||||
Allow: false,
|
|
||||||
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
|
||||||
}},
|
|
||||||
CSS: "",
|
|
||||||
FileSystem: fileutils.Dir(directory),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the default settings for Hugo.
|
|
||||||
hugo := &plugins.Hugo{
|
|
||||||
Root: directory,
|
|
||||||
Public: filepath.Join(directory, "public"),
|
|
||||||
Args: []string{},
|
|
||||||
CleanPublic: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find the Hugo executable path.
|
|
||||||
if err = hugo.Find(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attaches Hugo plugin to this file manager instance.
|
|
||||||
err = m.ActivatePlugin("hugo", hugo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.SetBaseURL(admin)
|
|
||||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
|
||||||
configs = append(configs, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
return configs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
|
||||||
func (p plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
for i := range p.Configs {
|
|
||||||
// Checks if this Path should be handled by File Manager.
|
|
||||||
if !httpserver.Path(r.URL.Path).Matches(p.Configs[i].BaseURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Configs[i].ServeHTTP(w, r)
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.Next.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterPlugin("hugo", caddy.Plugin{
|
caddy.RegisterPlugin("hugo", caddy.Plugin{
|
||||||
ServerType: "http",
|
ServerType: "http",
|
||||||
@@ -166,3 +21,32 @@ type plugin struct {
|
|||||||
Next httpserver.Handler
|
Next httpserver.Handler
|
||||||
Configs []*filemanager.FileManager
|
Configs []*filemanager.FileManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||||
|
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
for i := range f.Configs {
|
||||||
|
// Checks if this Path should be handled by File Manager.
|
||||||
|
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Handler(f.Configs[i]).ServeHTTP(w, r)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup configures a new FileManager middleware instance.
|
||||||
|
func setup(c *caddy.Controller) error {
|
||||||
|
configs, err := parser.Parse(c, "hugo")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||||
|
return plugin{Configs: configs, Next: next}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
52
caddy/jekyll/jekyll.go
Normal file
52
caddy/jekyll/jekyll.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package jekyll
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/hacdias/filemanager"
|
||||||
|
"github.com/hacdias/filemanager/caddy/parser"
|
||||||
|
h "github.com/hacdias/filemanager/http"
|
||||||
|
"github.com/mholt/caddy"
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy.RegisterPlugin("jekyll", caddy.Plugin{
|
||||||
|
ServerType: "http",
|
||||||
|
Action: setup,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type plugin struct {
|
||||||
|
Next httpserver.Handler
|
||||||
|
Configs []*filemanager.FileManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||||
|
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
for i := range f.Configs {
|
||||||
|
// Checks if this Path should be handled by File Manager.
|
||||||
|
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Handler(f.Configs[i]).ServeHTTP(w, r)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup configures a new FileManager middleware instance.
|
||||||
|
func setup(c *caddy.Controller) error {
|
||||||
|
configs, err := parser.Parse(c, "jekyll")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||||
|
return plugin{Configs: configs, Next: next}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
278
caddy/parser/parser.go
Normal file
278
caddy/parser/parser.go
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/asdine/storm"
|
||||||
|
"github.com/hacdias/filemanager"
|
||||||
|
"github.com/hacdias/filemanager/bolt"
|
||||||
|
"github.com/hacdias/filemanager/staticgen"
|
||||||
|
"github.com/hacdias/fileutils"
|
||||||
|
"github.com/mholt/caddy"
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
var databases = map[string]*storm.DB{}
|
||||||
|
|
||||||
|
// Parse ...
|
||||||
|
func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) {
|
||||||
|
var (
|
||||||
|
configs []*filemanager.FileManager
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
for c.Next() {
|
||||||
|
u := &filemanager.User{
|
||||||
|
Locale: "en",
|
||||||
|
AllowCommands: true,
|
||||||
|
AllowEdit: true,
|
||||||
|
AllowNew: true,
|
||||||
|
AllowPublish: true,
|
||||||
|
Commands: []string{"git", "svn", "hg"},
|
||||||
|
CSS: "",
|
||||||
|
Rules: []*filemanager.Rule{{
|
||||||
|
Regex: true,
|
||||||
|
Allow: false,
|
||||||
|
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := "/"
|
||||||
|
scope := "."
|
||||||
|
database := ""
|
||||||
|
noAuth := false
|
||||||
|
|
||||||
|
if plugin != "" {
|
||||||
|
baseURL = "/admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the baseURL and scope
|
||||||
|
args := c.RemainingArgs()
|
||||||
|
|
||||||
|
if plugin == "" {
|
||||||
|
if len(args) >= 1 {
|
||||||
|
baseURL = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
scope = args[1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(args) >= 1 {
|
||||||
|
scope = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
baseURL = args[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for c.NextBlock() {
|
||||||
|
switch c.Val() {
|
||||||
|
case "database":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
database = c.Val()
|
||||||
|
case "locale":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Locale = c.Val()
|
||||||
|
case "allow_commands":
|
||||||
|
if !c.NextArg() {
|
||||||
|
u.AllowCommands = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.AllowCommands, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "allow_edit":
|
||||||
|
if !c.NextArg() {
|
||||||
|
u.AllowEdit = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.AllowEdit, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "allow_new":
|
||||||
|
if !c.NextArg() {
|
||||||
|
u.AllowNew = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.AllowNew, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "allow_publish":
|
||||||
|
if !c.NextArg() {
|
||||||
|
u.AllowPublish = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.AllowPublish, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "commands":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Commands = strings.Split(c.Val(), " ")
|
||||||
|
case "css":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
file := c.Val()
|
||||||
|
css, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.CSS = string(css)
|
||||||
|
case "view_mode":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.ViewMode = c.Val()
|
||||||
|
if u.ViewMode != "mosaic" && u.ViewMode != "list" {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
case "no_auth":
|
||||||
|
if !c.NextArg() {
|
||||||
|
noAuth = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
noAuth, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
caddyConf := httpserver.GetConfig(c)
|
||||||
|
|
||||||
|
path := filepath.Join(caddy.AssetsPath(), "filemanager")
|
||||||
|
err := os.MkdirAll(path, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a database path and it is not absolute,
|
||||||
|
// it will be relative to Caddy folder.
|
||||||
|
if !filepath.IsAbs(database) && database != "" {
|
||||||
|
database = filepath.Join(path, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no database path on the settings,
|
||||||
|
// store one in .caddy/filemanager/name.db.
|
||||||
|
if database == "" {
|
||||||
|
// The name of the database is the hashed value of a string composed
|
||||||
|
// by the host, address path and the baseurl of this File Manager
|
||||||
|
// instance.
|
||||||
|
hasher := md5.New()
|
||||||
|
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL))
|
||||||
|
sha := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
database = filepath.Join(path, sha+".db")
|
||||||
|
|
||||||
|
fmt.Println("[WARNING] A database is going to be created for your File Manager instace at " + database +
|
||||||
|
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Scope = scope
|
||||||
|
u.FileSystem = fileutils.Dir(scope)
|
||||||
|
|
||||||
|
var db *storm.DB
|
||||||
|
if stored, ok := databases[database]; ok {
|
||||||
|
db = stored
|
||||||
|
} else {
|
||||||
|
db, err = storm.Open(database)
|
||||||
|
databases[database] = db
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &filemanager.FileManager{
|
||||||
|
NoAuth: noAuth,
|
||||||
|
BaseURL: "",
|
||||||
|
PrefixURL: "",
|
||||||
|
DefaultUser: u,
|
||||||
|
Store: &filemanager.Store{
|
||||||
|
Config: bolt.ConfigStore{DB: db},
|
||||||
|
Users: bolt.UsersStore{DB: db},
|
||||||
|
Share: bolt.ShareStore{DB: db},
|
||||||
|
},
|
||||||
|
NewFS: func(scope string) filemanager.FileSystem {
|
||||||
|
return fileutils.Dir(scope)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.Setup()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch plugin {
|
||||||
|
case "hugo":
|
||||||
|
// Initialize the default settings for Hugo.
|
||||||
|
hugo := &staticgen.Hugo{
|
||||||
|
Root: scope,
|
||||||
|
Public: filepath.Join(scope, "public"),
|
||||||
|
Args: []string{},
|
||||||
|
CleanPublic: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches Hugo plugin to this file manager instance.
|
||||||
|
err = m.Attach(hugo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "jekyll":
|
||||||
|
// Initialize the default settings for Jekyll.
|
||||||
|
jekyll := &staticgen.Jekyll{
|
||||||
|
Root: scope,
|
||||||
|
Public: filepath.Join(scope, "_site"),
|
||||||
|
Args: []string{},
|
||||||
|
CleanPublic: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches Hugo plugin to this file manager instance.
|
||||||
|
err = m.Attach(jekyll)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.NoAuth = noAuth
|
||||||
|
m.SetBaseURL(baseURL)
|
||||||
|
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
||||||
|
|
||||||
|
configs = append(configs, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
@@ -10,11 +10,14 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/asdine/storm"
|
||||||
|
|
||||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
|
||||||
"github.com/hacdias/filemanager/plugins"
|
|
||||||
|
|
||||||
"github.com/hacdias/filemanager"
|
"github.com/hacdias/filemanager"
|
||||||
|
"github.com/hacdias/filemanager/bolt"
|
||||||
|
h "github.com/hacdias/filemanager/http"
|
||||||
|
"github.com/hacdias/filemanager/staticgen"
|
||||||
"github.com/hacdias/fileutils"
|
"github.com/hacdias/fileutils"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@@ -27,11 +30,18 @@ var (
|
|||||||
scope string
|
scope string
|
||||||
commands string
|
commands string
|
||||||
logfile string
|
logfile string
|
||||||
plugin string
|
staticg string
|
||||||
|
locale string
|
||||||
|
baseurl string
|
||||||
|
prefixurl string
|
||||||
|
viewMode string
|
||||||
port int
|
port int
|
||||||
|
noAuth bool
|
||||||
allowCommands bool
|
allowCommands bool
|
||||||
allowEdit bool
|
allowEdit bool
|
||||||
allowNew bool
|
allowNew bool
|
||||||
|
allowPublish bool
|
||||||
|
showVer bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -41,11 +51,18 @@ func init() {
|
|||||||
flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file")
|
flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file")
|
||||||
flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file")
|
flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file")
|
||||||
flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users")
|
flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users")
|
||||||
|
flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL")
|
||||||
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
|
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
|
||||||
|
flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL")
|
||||||
|
flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users")
|
||||||
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow 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(&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(&allowNew, "allow-new", true, "Default allow new option for new users")
|
||||||
flag.StringVar(&plugin, "plugin", "", "Plugin you want to enable")
|
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
|
||||||
|
flag.StringVar(&locale, "locale", "en", "Default locale for new users")
|
||||||
|
flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable")
|
||||||
|
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupViper() {
|
func setupViper() {
|
||||||
@@ -58,7 +75,13 @@ func setupViper() {
|
|||||||
viper.SetDefault("AllowCommmands", true)
|
viper.SetDefault("AllowCommmands", true)
|
||||||
viper.SetDefault("AllowEdit", true)
|
viper.SetDefault("AllowEdit", true)
|
||||||
viper.SetDefault("AllowNew", true)
|
viper.SetDefault("AllowNew", true)
|
||||||
viper.SetDefault("Plugin", "")
|
viper.SetDefault("AllowPublish", true)
|
||||||
|
viper.SetDefault("StaticGen", "")
|
||||||
|
viper.SetDefault("Locale", "en")
|
||||||
|
viper.SetDefault("NoAuth", false)
|
||||||
|
viper.SetDefault("BaseURL", "")
|
||||||
|
viper.SetDefault("PrefixURL", "")
|
||||||
|
viper.SetDefault("ViewMode", "mosaic")
|
||||||
|
|
||||||
viper.BindPFlag("Port", flag.Lookup("port"))
|
viper.BindPFlag("Port", flag.Lookup("port"))
|
||||||
viper.BindPFlag("Address", flag.Lookup("address"))
|
viper.BindPFlag("Address", flag.Lookup("address"))
|
||||||
@@ -69,16 +92,31 @@ func setupViper() {
|
|||||||
viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
|
viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
|
||||||
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
|
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
|
||||||
viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
|
viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
|
||||||
viper.BindPFlag("Plugin", flag.Lookup("plugin"))
|
viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish"))
|
||||||
|
viper.BindPFlag("Locale", flag.Lookup("locale"))
|
||||||
|
viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
|
||||||
|
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
|
||||||
|
viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
|
||||||
|
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
|
||||||
|
viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
|
||||||
|
|
||||||
viper.SetConfigName("filemanager")
|
viper.SetConfigName("filemanager")
|
||||||
viper.AddConfigPath(".")
|
viper.AddConfigPath(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printVersion() {
|
||||||
|
fmt.Println("filemanager version", filemanager.Version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
setupViper()
|
setupViper()
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if showVer {
|
||||||
|
printVersion()
|
||||||
|
}
|
||||||
|
|
||||||
// Add a configuration file if set.
|
// Add a configuration file if set.
|
||||||
if config != "" {
|
if config != "" {
|
||||||
ext := filepath.Ext(config)
|
ext := filepath.Ext(config)
|
||||||
@@ -118,40 +156,6 @@ func main() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a File Manager instance.
|
|
||||||
fm, err := filemanager.New(viper.GetString("Database"), filemanager.User{
|
|
||||||
AllowCommands: viper.GetBool("AllowCommands"),
|
|
||||||
AllowEdit: viper.GetBool("AllowEdit"),
|
|
||||||
AllowNew: viper.GetBool("AllowNew"),
|
|
||||||
Commands: viper.GetStringSlice("Commands"),
|
|
||||||
Rules: []*filemanager.Rule{},
|
|
||||||
CSS: "",
|
|
||||||
FileSystem: fileutils.Dir(viper.GetString("Scope")),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if viper.GetString("Plugin") == "hugo" {
|
|
||||||
// Initialize the default settings for Hugo.
|
|
||||||
hugo := &plugins.Hugo{
|
|
||||||
Root: viper.GetString("Scope"),
|
|
||||||
Public: filepath.Join(viper.GetString("Scope"), "public"),
|
|
||||||
Args: []string{},
|
|
||||||
CleanPublic: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find the Hugo executable path.
|
|
||||||
if err = hugo.Find(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = fm.ActivatePlugin("hugo", hugo); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builds the address and a listener.
|
// Builds the address and a listener.
|
||||||
laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
|
laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
|
||||||
listener, err := net.Listen("tcp", laddr)
|
listener, err := net.Listen("tcp", laddr)
|
||||||
@@ -163,7 +167,73 @@ func main() {
|
|||||||
fmt.Println("Listening on", listener.Addr().String())
|
fmt.Println("Listening on", listener.Addr().String())
|
||||||
|
|
||||||
// Starts the server.
|
// Starts the server.
|
||||||
if err := http.Serve(listener, fm); err != nil {
|
if err := http.Serve(listener, handler()); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handler() http.Handler {
|
||||||
|
db, err := storm.Open(viper.GetString("Database"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := &filemanager.FileManager{
|
||||||
|
NoAuth: viper.GetBool("NoAuth"),
|
||||||
|
BaseURL: viper.GetString("BaseURL"),
|
||||||
|
PrefixURL: viper.GetString("PrefixURL"),
|
||||||
|
DefaultUser: &filemanager.User{
|
||||||
|
AllowCommands: viper.GetBool("AllowCommands"),
|
||||||
|
AllowEdit: viper.GetBool("AllowEdit"),
|
||||||
|
AllowNew: viper.GetBool("AllowNew"),
|
||||||
|
AllowPublish: viper.GetBool("AllowPublish"),
|
||||||
|
Commands: viper.GetStringSlice("Commands"),
|
||||||
|
Rules: []*filemanager.Rule{},
|
||||||
|
Locale: viper.GetString("Locale"),
|
||||||
|
CSS: "",
|
||||||
|
Scope: viper.GetString("Scope"),
|
||||||
|
FileSystem: fileutils.Dir(viper.GetString("Scope")),
|
||||||
|
ViewMode: viper.GetString("ViewMode"),
|
||||||
|
},
|
||||||
|
Store: &filemanager.Store{
|
||||||
|
Config: bolt.ConfigStore{DB: db},
|
||||||
|
Users: bolt.UsersStore{DB: db},
|
||||||
|
Share: bolt.ShareStore{DB: db},
|
||||||
|
},
|
||||||
|
NewFS: func(scope string) filemanager.FileSystem {
|
||||||
|
return fileutils.Dir(scope)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fm.Setup()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch viper.GetString("StaticGen") {
|
||||||
|
case "hugo":
|
||||||
|
hugo := &staticgen.Hugo{
|
||||||
|
Root: viper.GetString("Scope"),
|
||||||
|
Public: filepath.Join(viper.GetString("Scope"), "public"),
|
||||||
|
Args: []string{},
|
||||||
|
CleanPublic: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = fm.Attach(hugo); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
case "jekyll":
|
||||||
|
jekyll := &staticgen.Jekyll{
|
||||||
|
Root: viper.GetString("Scope"),
|
||||||
|
Public: filepath.Join(viper.GetString("Scope"), "_site"),
|
||||||
|
Args: []string{"build"},
|
||||||
|
CleanPublic: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = fm.Attach(jekyll); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Handler(fm)
|
||||||
|
}
|
||||||
|
|||||||
73
doc.go
Normal file
73
doc.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
Package filemanager provides a web interface to access your files
|
||||||
|
wherever you are. To use this package as a middleware for your app,
|
||||||
|
you'll need to import both File Manager and File Manager HTTP packages.
|
||||||
|
|
||||||
|
import (
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
|
h "github.com/hacdias/filemanager/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
Then, you should create a new FileManager object with your options. In this
|
||||||
|
case, I'm using BoltDB (via Storm package) as a Store. So, you'll also need
|
||||||
|
to import "github.com/hacdias/filemanager/bolt".
|
||||||
|
|
||||||
|
db, _ := storm.Open("bolt.db")
|
||||||
|
|
||||||
|
m := &fm.FileManager{
|
||||||
|
NoAuth: false,
|
||||||
|
DefaultUser: &fm.User{
|
||||||
|
AllowCommands: true,
|
||||||
|
AllowEdit: true,
|
||||||
|
AllowNew: true,
|
||||||
|
AllowPublish: true,
|
||||||
|
Commands: []string{"git"},
|
||||||
|
Rules: []*fm.Rule{},
|
||||||
|
Locale: "en",
|
||||||
|
CSS: "",
|
||||||
|
Scope: ".",
|
||||||
|
FileSystem: fileutils.Dir("."),
|
||||||
|
},
|
||||||
|
Store: &fm.Store{
|
||||||
|
Config: bolt.ConfigStore{DB: db},
|
||||||
|
Users: bolt.UsersStore{DB: db},
|
||||||
|
Share: bolt.ShareStore{DB: db},
|
||||||
|
},
|
||||||
|
NewFS: func(scope string) fm.FileSystem {
|
||||||
|
return fileutils.Dir(scope)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
The credentials for the first user are always 'admin' for both the user and
|
||||||
|
the password, and they can be changed later through the settings. The first
|
||||||
|
user is always an Admin and has all of the permissions set to 'true'.
|
||||||
|
|
||||||
|
Then, you should set the Prefix URL and the Base URL, using the following
|
||||||
|
functions:
|
||||||
|
|
||||||
|
m.SetBaseURL("/")
|
||||||
|
m.SetPrefixURL("/")
|
||||||
|
|
||||||
|
The Prefix URL is a part of the path that is already stripped from the
|
||||||
|
r.URL.Path variable before the request arrives to File Manager's handler.
|
||||||
|
This is a function that will rarely be used. You can see one example on Caddy
|
||||||
|
filemanager plugin.
|
||||||
|
|
||||||
|
The Base URL is the URL path where you want File Manager to be available in. If
|
||||||
|
you want to be available at the root path, you should call:
|
||||||
|
|
||||||
|
m.SetBaseURL("/")
|
||||||
|
|
||||||
|
But if you want to access it at '/admin', you would call:
|
||||||
|
|
||||||
|
m.SetBaseURL("/admin")
|
||||||
|
|
||||||
|
Now, that you already have a File Manager instance created, you just need to
|
||||||
|
add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
|
||||||
|
We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
|
||||||
|
|
||||||
|
One simple implementation for this, at port 80, in the root of the domain, would be:
|
||||||
|
|
||||||
|
http.ListenAndServe(":80", h.Handler(m))
|
||||||
|
*/
|
||||||
|
package filemanager
|
||||||
74
file.go
74
file.go
@@ -7,7 +7,6 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -20,16 +19,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/hugo/parser"
|
"github.com/gohugoio/hugo/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// File contains the information about a particular file or directory.
|
||||||
errInvalidOption = errors.New("Invalid option")
|
type File struct {
|
||||||
)
|
// Indicates the Kind of view on the front-end (Listing, editor or preview).
|
||||||
|
|
||||||
// file contains the information about a particular file or directory.
|
|
||||||
type file struct {
|
|
||||||
// Indicates the Kind of view on the front-end (listing, editor or preview).
|
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
// The name of the file.
|
// The name of the file.
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -54,37 +49,35 @@ type file struct {
|
|||||||
// Stores the content of a text file.
|
// Stores the content of a text file.
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
|
|
||||||
*listing `json:",omitempty"`
|
*Listing `json:",omitempty"`
|
||||||
|
|
||||||
Metadata string `json:"metadata,omitempty"`
|
Metadata string `json:"metadata,omitempty"`
|
||||||
Language string `json:"language,omitempty"`
|
Language string `json:"language,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// A listing is the context used to fill out a template.
|
// A Listing is the context used to fill out a template.
|
||||||
type listing struct {
|
type Listing struct {
|
||||||
// The items (files and folders) in the path.
|
// The items (files and folders) in the path.
|
||||||
Items []*file `json:"items"`
|
Items []*File `json:"items"`
|
||||||
// The number of directories in the listing.
|
// The number of directories in the Listing.
|
||||||
NumDirs int `json:"numDirs"`
|
NumDirs int `json:"numDirs"`
|
||||||
// The number of files (items that aren't directories) in the listing.
|
// The number of files (items that aren't directories) in the Listing.
|
||||||
NumFiles int `json:"numFiles"`
|
NumFiles int `json:"numFiles"`
|
||||||
// Which sorting order is used.
|
// Which sorting order is used.
|
||||||
Sort string `json:"sort"`
|
Sort string `json:"sort"`
|
||||||
// And which order.
|
// And which order.
|
||||||
Order string `json:"order"`
|
Order string `json:"order"`
|
||||||
// Displays in mosaic or list.
|
|
||||||
Display string `json:"display"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getInfo gets the file information and, in case of error, returns the
|
// GetInfo gets the file information and, in case of error, returns the
|
||||||
// respective HTTP error code
|
// respective HTTP error code
|
||||||
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
|
func GetInfo(url *url.URL, c *FileManager, u *User) (*File, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
i := &file{
|
i := &File{
|
||||||
URL: "/files" + url.String(),
|
URL: "/files" + url.String(),
|
||||||
VirtualPath: url.Path,
|
VirtualPath: url.Path,
|
||||||
Path: filepath.Join(string(u.FileSystem), url.Path),
|
Path: filepath.Join(u.Scope, url.Path),
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := u.FileSystem.Stat(url.Path)
|
info, err := u.FileSystem.Stat(url.Path)
|
||||||
@@ -106,11 +99,11 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
|
|||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getListing gets the information about a specific directory and its files.
|
// GetListing gets the information about a specific directory and its files.
|
||||||
func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
func (i *File) GetListing(u *User, r *http.Request) error {
|
||||||
// Gets the directory information using the Virtual File System of
|
// Gets the directory information using the Virtual File System of
|
||||||
// the user configuration.
|
// the user configuration.
|
||||||
f, err := c.User.FileSystem.OpenFile(c.FI.VirtualPath, os.O_RDONLY, 0)
|
f, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -123,7 +116,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fileinfos []*file
|
fileinfos []*File
|
||||||
dirCount, fileCount int
|
dirCount, fileCount int
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,7 +127,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
|||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
name := f.Name()
|
name := f.Name()
|
||||||
allowed := c.User.Allowed("/" + name)
|
allowed := u.Allowed("/" + name)
|
||||||
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
continue
|
continue
|
||||||
@@ -150,7 +143,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
|||||||
// Absolute URL
|
// Absolute URL
|
||||||
url := url.URL{Path: baseurl + name}
|
url := url.URL{Path: baseurl + name}
|
||||||
|
|
||||||
i := &file{
|
i := &File{
|
||||||
Name: f.Name(),
|
Name: f.Name(),
|
||||||
Size: f.Size(),
|
Size: f.Size(),
|
||||||
ModTime: f.ModTime(),
|
ModTime: f.ModTime(),
|
||||||
@@ -166,7 +159,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
|||||||
fileinfos = append(fileinfos, i)
|
fileinfos = append(fileinfos, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
i.listing = &listing{
|
i.Listing = &Listing{
|
||||||
Items: fileinfos,
|
Items: fileinfos,
|
||||||
NumDirs: dirCount,
|
NumDirs: dirCount,
|
||||||
NumFiles: fileCount,
|
NumFiles: fileCount,
|
||||||
@@ -175,8 +168,8 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEditor gets the editor based on a Info struct
|
// GetEditor gets the editor based on a Info struct
|
||||||
func (i *file) getEditor() error {
|
func (i *File) GetEditor() error {
|
||||||
i.Language = editorLanguage(i.Extension)
|
i.Language = editorLanguage(i.Extension)
|
||||||
// If the editor will hold only content, leave now.
|
// If the editor will hold only content, leave now.
|
||||||
if editorMode(i.Language) == "content" {
|
if editorMode(i.Language) == "content" {
|
||||||
@@ -205,7 +198,7 @@ func (i *file) getEditor() error {
|
|||||||
|
|
||||||
// GetFileType obtains the mimetype and converts it to a simple
|
// GetFileType obtains the mimetype and converts it to a simple
|
||||||
// type nomenclature.
|
// type nomenclature.
|
||||||
func (i *file) GetFileType(checkContent bool) error {
|
func (i *File) GetFileType(checkContent bool) error {
|
||||||
var content []byte
|
var content []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -283,7 +276,8 @@ End:
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i file) Checksum(kind string) (string, error) {
|
// Checksum retrieves the checksum of a file.
|
||||||
|
func (i File) Checksum(algo string) (string, error) {
|
||||||
file, err := os.Open(i.Path)
|
file, err := os.Open(i.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -293,7 +287,7 @@ func (i file) Checksum(kind string) (string, error) {
|
|||||||
|
|
||||||
var h hash.Hash
|
var h hash.Hash
|
||||||
|
|
||||||
switch kind {
|
switch algo {
|
||||||
case "md5":
|
case "md5":
|
||||||
h = md5.New()
|
h = md5.New()
|
||||||
case "sha1":
|
case "sha1":
|
||||||
@@ -303,7 +297,7 @@ func (i file) Checksum(kind string) (string, error) {
|
|||||||
case "sha512":
|
case "sha512":
|
||||||
h = sha512.New()
|
h = sha512.New()
|
||||||
default:
|
default:
|
||||||
return "", errInvalidOption
|
return "", ErrInvalidOption
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(h, file)
|
_, err = io.Copy(h, file)
|
||||||
@@ -315,12 +309,12 @@ func (i file) Checksum(kind string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CanBeEdited checks if the extension of a file is supported by the editor
|
// CanBeEdited checks if the extension of a file is supported by the editor
|
||||||
func (i file) CanBeEdited() bool {
|
func (i File) CanBeEdited() bool {
|
||||||
return i.Type == "text"
|
return i.Type == "text"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplySort applies the sort order using .Order and .Sort
|
// ApplySort applies the sort order using .Order and .Sort
|
||||||
func (l listing) ApplySort() {
|
func (l Listing) ApplySort() {
|
||||||
// Check '.Order' to know how to sort
|
// Check '.Order' to know how to sort
|
||||||
if l.Order == "desc" {
|
if l.Order == "desc" {
|
||||||
switch l.Sort {
|
switch l.Sort {
|
||||||
@@ -349,10 +343,10 @@ func (l listing) ApplySort() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement sorting for listing
|
// Implement sorting for Listing
|
||||||
type byName listing
|
type byName Listing
|
||||||
type bySize listing
|
type bySize Listing
|
||||||
type byModified listing
|
type byModified Listing
|
||||||
|
|
||||||
// By Name
|
// By Name
|
||||||
func (l byName) Len() int {
|
func (l byName) Len() int {
|
||||||
|
|||||||
709
filemanager.go
709
filemanager.go
@@ -1,60 +1,9 @@
|
|||||||
// Package filemanager provides a web interface to access your files
|
|
||||||
// wherever you are. To use this package as a middleware for your app,
|
|
||||||
// you'll need to create a filemanager instance:
|
|
||||||
//
|
|
||||||
// m, err := filemanager.New(database, user)
|
|
||||||
//
|
|
||||||
// Where 'user' contains the default options for new users. You can just
|
|
||||||
// use 'filemanager.DefaultUser' or create yourself a default user:
|
|
||||||
//
|
|
||||||
// m, err := filemanager.New(database, filemanager.User{
|
|
||||||
// Admin: false,
|
|
||||||
// AllowCommands: false,
|
|
||||||
// AllowEdit: true,
|
|
||||||
// AllowNew: true,
|
|
||||||
// Commands: []string{
|
|
||||||
// "git",
|
|
||||||
// },
|
|
||||||
// Rules: []*filemanager.Rule{},
|
|
||||||
// CSS: "",
|
|
||||||
// FileSystem: webdav.Dir("/path/to/files"),
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// The credentials for the first user are always 'admin' for both the user and
|
|
||||||
// the password, and they can be changed later through the settings. The first
|
|
||||||
// user is always an Admin and has all of the permissions set to 'true'.
|
|
||||||
//
|
|
||||||
// Then, you should set the Prefix URL and the Base URL, using the following
|
|
||||||
// functions:
|
|
||||||
//
|
|
||||||
// m.SetBaseURL("/")
|
|
||||||
// m.SetPrefixURL("/")
|
|
||||||
//
|
|
||||||
// The Prefix URL is a part of the path that is already stripped from the
|
|
||||||
// r.URL.Path variable before the request arrives to File Manager's handler.
|
|
||||||
// This is a function that will rarely be used. You can see one example on Caddy
|
|
||||||
// filemanager plugin.
|
|
||||||
//
|
|
||||||
// The Base URL is the URL path where you want File Manager to be available in. If
|
|
||||||
// you want to be available at the root path, you should call:
|
|
||||||
//
|
|
||||||
// m.SetBaseURL("/")
|
|
||||||
//
|
|
||||||
// But if you want to access it at '/admin', you would call:
|
|
||||||
//
|
|
||||||
// m.SetBaseURL("/admin")
|
|
||||||
//
|
|
||||||
// Now, that you already have a File Manager instance created, you just need to
|
|
||||||
// add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
|
|
||||||
// We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
|
|
||||||
//
|
|
||||||
// One simple implementation for this, at port 80, in the root of the domain, would be:
|
|
||||||
//
|
|
||||||
// http.ListenAndServe(":80", m)
|
|
||||||
package filemanager
|
package filemanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -62,32 +11,46 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
rice "github.com/GeertJohan/go.rice"
|
rice "github.com/GeertJohan/go.rice"
|
||||||
"github.com/asdine/storm"
|
|
||||||
"github.com/hacdias/fileutils"
|
"github.com/hacdias/fileutils"
|
||||||
"github.com/mholt/caddy"
|
"github.com/mholt/caddy"
|
||||||
|
"github.com/robfig/cron"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Version is the current File Manager version.
|
||||||
|
const Version = "1.3.2"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errUserExist = errors.New("user already exists")
|
ErrExist = errors.New("the resource already exists")
|
||||||
errUserNotExist = errors.New("user does not exist")
|
ErrNotExist = errors.New("the resource does not exist")
|
||||||
errEmptyRequest = errors.New("request body is empty")
|
ErrEmptyRequest = errors.New("request body is empty")
|
||||||
errEmptyPassword = errors.New("password is empty")
|
ErrEmptyPassword = errors.New("password is empty")
|
||||||
plugins = map[string]Plugin{}
|
ErrEmptyUsername = errors.New("username is empty")
|
||||||
|
ErrEmptyScope = errors.New("scope is empty")
|
||||||
|
ErrWrongDataType = errors.New("wrong data type")
|
||||||
|
ErrInvalidUpdateField = errors.New("invalid field to update")
|
||||||
|
ErrInvalidOption = errors.New("Invalid option")
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileManager is a file manager instance. It should be creating using the
|
// FileManager is a file manager instance. It should be creating using the
|
||||||
// 'New' function and not directly.
|
// 'New' function and not directly.
|
||||||
type FileManager struct {
|
type FileManager struct {
|
||||||
// The BoltDB database for this instance.
|
// Cron job to manage schedulings.
|
||||||
db *storm.DB
|
Cron *cron.Cron
|
||||||
|
|
||||||
// The key used to sign the JWT tokens.
|
// The key used to sign the JWT tokens.
|
||||||
key []byte
|
Key []byte
|
||||||
|
|
||||||
// The static assets.
|
// The static assets.
|
||||||
assets *rice.Box
|
Assets *rice.Box
|
||||||
|
|
||||||
|
// The Store is used to manage users, shareable links and
|
||||||
|
// other stuff that is saved on the database.
|
||||||
|
Store *Store
|
||||||
|
|
||||||
// PrefixURL is a part of the URL that is already trimmed from the request URL before it
|
// 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
|
// arrives to our handlers. It may be useful when using File Manager as a middleware
|
||||||
@@ -99,194 +62,130 @@ type FileManager struct {
|
|||||||
// edited directly. Use SetBaseURL.
|
// edited directly. Use SetBaseURL.
|
||||||
BaseURL string
|
BaseURL string
|
||||||
|
|
||||||
|
// NoAuth disables the authentication. When the authentication is disabled,
|
||||||
|
// there will only exist one user, called "admin".
|
||||||
|
NoAuth bool
|
||||||
|
|
||||||
|
// StaticGen is the static websit generator handler.
|
||||||
|
StaticGen StaticGen
|
||||||
|
|
||||||
// The Default User needed to build the New User page.
|
// The Default User needed to build the New User page.
|
||||||
DefaultUser *User
|
DefaultUser *User
|
||||||
|
|
||||||
// Users is a map with the different configurations for each user.
|
|
||||||
Users map[string]*User
|
|
||||||
|
|
||||||
// A map of events to a slice of commands.
|
// A map of events to a slice of commands.
|
||||||
Commands map[string][]string
|
Commands map[string][]string
|
||||||
|
|
||||||
// The options of the plugins that have been plugged into this instance.
|
// Global stylesheet.
|
||||||
Plugins map[string]interface{}
|
CSS string
|
||||||
|
|
||||||
|
// NewFS should build a new file system for a given path.
|
||||||
|
NewFS FSBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandEvents = []string{
|
||||||
|
"before_save",
|
||||||
|
"after_save",
|
||||||
|
"before_publish",
|
||||||
|
"after_publish",
|
||||||
|
"before_copy",
|
||||||
|
"after_copy",
|
||||||
|
"before_rename",
|
||||||
|
"after_rename",
|
||||||
|
"before_upload",
|
||||||
|
"after_upload",
|
||||||
|
"before_delete",
|
||||||
|
"after_delete",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command is a command function.
|
// Command is a command function.
|
||||||
type Command func(r *http.Request, m *FileManager, u *User) error
|
type Command func(r *http.Request, m *FileManager, u *User) error
|
||||||
|
|
||||||
// User contains the configuration for each user.
|
// FSBuilder is the File System Builder.
|
||||||
type User struct {
|
type FSBuilder func(scope string) FileSystem
|
||||||
// ID is the required primary key with auto increment0
|
|
||||||
ID int `storm:"id,increment"`
|
|
||||||
|
|
||||||
// Username is the user username used to login.
|
// Setup loads the configuration from the database and configures
|
||||||
Username string `json:"username" storm:"index,unique"`
|
// the Assets and the Cron job. It must always be run after
|
||||||
|
// creating a File Manager object.
|
||||||
// The hashed password. This never reaches the front-end because it's temporarily
|
func (m *FileManager) Setup() error {
|
||||||
// emptied during JSON marshall.
|
|
||||||
Password string `json:"password"`
|
|
||||||
|
|
||||||
// Tells if this user is an admin.
|
|
||||||
Admin bool `json:"admin"`
|
|
||||||
|
|
||||||
// FileSystem is the virtual file system the user has access.
|
|
||||||
FileSystem fileutils.Dir `json:"filesystem"`
|
|
||||||
|
|
||||||
// Rules is an array of access and deny rules.
|
|
||||||
Rules []*Rule `json:"rules"`
|
|
||||||
|
|
||||||
// Custom styles for this user.
|
|
||||||
CSS string `json:"css"`
|
|
||||||
|
|
||||||
// These indicate if the user can perform certain actions.
|
|
||||||
AllowNew bool `json:"allowNew"` // Create files and folders
|
|
||||||
AllowEdit bool `json:"allowEdit"` // Edit/rename files
|
|
||||||
AllowCommands bool `json:"allowCommands"` // Execute commands
|
|
||||||
Permissions map[string]bool `json:"permissions"` // Permissions added by plugins
|
|
||||||
|
|
||||||
// Commands is the list of commands the user can execute.
|
|
||||||
Commands []string `json:"commands"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rule is a dissalow/allow rule.
|
|
||||||
type Rule struct {
|
|
||||||
// Regex indicates if this rule uses Regular Expressions or not.
|
|
||||||
Regex bool `json:"regex"`
|
|
||||||
|
|
||||||
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
|
|
||||||
Allow bool `json:"allow"`
|
|
||||||
|
|
||||||
// Path is the corresponding URL path for this rule.
|
|
||||||
Path string `json:"path"`
|
|
||||||
|
|
||||||
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
|
|
||||||
Regexp *Regexp `json:"regexp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regexp is a regular expression wrapper around native regexp.
|
|
||||||
type Regexp struct {
|
|
||||||
Raw string `json:"raw"`
|
|
||||||
regexp *regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
type Plugin struct {
|
|
||||||
JavaScript string
|
|
||||||
CommandEvents []string
|
|
||||||
Permissions []Permission
|
|
||||||
Handler PluginHandler
|
|
||||||
Options interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Permission struct {
|
|
||||||
Name string
|
|
||||||
Value bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type PluginHandler interface {
|
|
||||||
// If the Plugin returns (0, nil), the executation of File Manager will procced as usual.
|
|
||||||
// Otherwise it will stop.
|
|
||||||
Before(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
|
||||||
After(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterPlugin(name string, plugin Plugin) {
|
|
||||||
if _, ok := plugins[name]; ok {
|
|
||||||
panic(name + " plugin is already registred")
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins[name] = plugin
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultUser is used on New, when no 'base' user is provided.
|
|
||||||
var DefaultUser = User{
|
|
||||||
AllowCommands: true,
|
|
||||||
AllowEdit: true,
|
|
||||||
AllowNew: true,
|
|
||||||
Permissions: map[string]bool{},
|
|
||||||
Commands: []string{},
|
|
||||||
Rules: []*Rule{},
|
|
||||||
CSS: "",
|
|
||||||
Admin: true,
|
|
||||||
FileSystem: fileutils.Dir("."),
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new File Manager instance. If 'database' file already
|
|
||||||
// exists, it will load the users from there. Otherwise, a new user
|
|
||||||
// will be created using the 'base' variable. The 'base' User should
|
|
||||||
// not have the Password field hashed.
|
|
||||||
func New(database string, base User) (*FileManager, error) {
|
|
||||||
// Creates a new File Manager instance with the Users
|
// Creates a new File Manager instance with the Users
|
||||||
// map and Assets box.
|
// map and Assets box.
|
||||||
m := &FileManager{
|
m.Assets = rice.MustFindBox("./assets/dist")
|
||||||
Users: map[string]*User{},
|
m.Cron = cron.New()
|
||||||
Plugins: map[string]interface{}{},
|
|
||||||
assets: rice.MustFindBox("./assets/dist"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tries to open a database on the location provided. This
|
|
||||||
// function will automatically create a new one if it doesn't
|
|
||||||
// exist.
|
|
||||||
db, err := storm.Open(database)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tries to get the encryption key from the database.
|
// Tries to get the encryption key from the database.
|
||||||
// If it doesn't exist, create a new one of 256 bits.
|
// If it doesn't exist, create a new one of 256 bits.
|
||||||
err = db.Get("config", "key", &m.key)
|
err := m.Store.Config.Get("key", &m.Key)
|
||||||
if err != nil && err == storm.ErrNotFound {
|
if err != nil && err == ErrNotExist {
|
||||||
var bytes []byte
|
var bytes []byte
|
||||||
bytes, err = generateRandomBytes(64)
|
bytes, err = GenerateRandomBytes(64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.key = bytes
|
m.Key = bytes
|
||||||
err = db.Set("config", "key", m.key)
|
err = m.Store.Config.Save("key", m.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the global CSS.
|
||||||
|
err = m.Store.Config.Get("css", &m.CSS)
|
||||||
|
if err != nil && err == ErrNotExist {
|
||||||
|
err = m.Store.Config.Save("css", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tries to get the event commands from the database.
|
// Tries to get the event commands from the database.
|
||||||
// If they don't exist, initialize them.
|
// If they don't exist, initialize them.
|
||||||
err = db.Get("config", "commands", &m.Commands)
|
err = m.Store.Config.Get("commands", &m.Commands)
|
||||||
if err != nil && err == storm.ErrNotFound {
|
|
||||||
m.Commands = map[string][]string{
|
if err == nil {
|
||||||
"before_save": {},
|
// Add hypothetically new command handlers.
|
||||||
"after_save": {},
|
for _, command := range commandEvents {
|
||||||
|
if _, ok := m.Commands[command]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Commands[command] = []string{}
|
||||||
}
|
}
|
||||||
err = db.Set("config", "commands", m.Commands)
|
}
|
||||||
|
|
||||||
|
if err != nil && err == ErrNotExist {
|
||||||
|
m.Commands = map[string][]string{}
|
||||||
|
|
||||||
|
// Initialize the command handlers.
|
||||||
|
for _, command := range commandEvents {
|
||||||
|
m.Commands[command] = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.Store.Config.Save("commands", m.Commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tries to fetch the users from the database and if there are
|
// Tries to fetch the users from the database.
|
||||||
// any, add them to the current File Manager instance.
|
users, err := m.Store.Users.Gets(m.NewFS)
|
||||||
var users []User
|
if err != nil && err != ErrNotExist {
|
||||||
err = db.All(&users)
|
return err
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range users {
|
|
||||||
m.Users[users[i].Username] = &users[i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are no users in the database, it creates a new one
|
// If there are no users in the database, it creates a new one
|
||||||
// based on 'base' User that must be provided by the function caller.
|
// based on 'base' User that must be provided by the function caller.
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
u := base
|
u := *m.DefaultUser
|
||||||
u.Username = "admin"
|
u.Username = "admin"
|
||||||
|
|
||||||
// Hashes the password.
|
// Hashes the password.
|
||||||
u.Password, err = hashPassword("admin")
|
u.Password, err = HashPassword("admin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The first user must be an administrator.
|
// The first user must be an administrator.
|
||||||
@@ -294,23 +193,21 @@ func New(database string, base User) (*FileManager, error) {
|
|||||||
u.AllowCommands = true
|
u.AllowCommands = true
|
||||||
u.AllowNew = true
|
u.AllowNew = true
|
||||||
u.AllowEdit = true
|
u.AllowEdit = true
|
||||||
|
u.AllowPublish = true
|
||||||
|
|
||||||
// Saves the user to the database.
|
// Saves the user to the database.
|
||||||
if err := db.Save(&u); err != nil {
|
if err := m.Store.Users.Save(&u); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Users[u.Username] = &u
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attaches db to this File Manager instance.
|
m.DefaultUser.Username = ""
|
||||||
m.db = db
|
m.DefaultUser.Password = ""
|
||||||
|
|
||||||
// Create the default user, making a copy of the base.
|
m.Cron.AddFunc("@hourly", m.ShareCleaner)
|
||||||
base.Username = ""
|
m.Cron.Start()
|
||||||
base.Password = ""
|
|
||||||
m.DefaultUser = &base
|
return nil
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RootURL returns the actual URL where
|
// RootURL returns the actual URL where
|
||||||
@@ -337,148 +234,50 @@ func (m *FileManager) SetBaseURL(url string) {
|
|||||||
m.BaseURL = strings.TrimSuffix(url, "/")
|
m.BaseURL = strings.TrimSuffix(url, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActivatePlugin activates a plugin to a File Manager instance and
|
// Attach attaches a static generator to the current File Manager.
|
||||||
// loads its options from the database.
|
func (m *FileManager) Attach(s StaticGen) error {
|
||||||
func (m *FileManager) ActivatePlugin(name string, options interface{}) error {
|
if reflect.TypeOf(s).Kind() != reflect.Ptr {
|
||||||
if reflect.TypeOf(options).Kind() != reflect.Ptr {
|
return errors.New("data should be a pointer to interface, not interface")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := s.Setup()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the command event hooks.
|
m.StaticGen = s
|
||||||
for _, evt := range plugin.CommandEvents {
|
|
||||||
if _, ok := m.Commands[evt]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Commands[evt] = []string{}
|
err = m.Store.Config.Get("staticgen_"+s.Name(), s)
|
||||||
|
if err == ErrNotExist {
|
||||||
|
return m.Store.Config.Save("staticgen_"+s.Name(), s)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.db.Set("config", "commands", m.Commands)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShareCleaner removes sharing links that are no longer active.
|
||||||
|
// This function is set to run periodically.
|
||||||
|
func (m FileManager) ShareCleaner() {
|
||||||
|
// Get all links.
|
||||||
|
links, err := m.Store.Share.Gets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Print(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the user permissions.
|
// Find the expired ones.
|
||||||
for _, perm := range plugin.Permissions {
|
for i := range links {
|
||||||
err = m.registerPermission(perm.Name, perm.Value)
|
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
|
||||||
if err != nil {
|
err = m.Store.Share.Delete(links[i].Hash)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
log.Print(err)
|
||||||
}
|
|
||||||
|
|
||||||
m.Plugins[name] = options
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerPermission registers a new user permission and adds it to every
|
|
||||||
// user with it default's 'value'. If the user is an admin, it will
|
|
||||||
// be true.
|
|
||||||
func (m *FileManager) registerPermission(name string, value bool) error {
|
|
||||||
if _, ok := m.DefaultUser.Permissions[name]; ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the default value for this permission on the default user.
|
|
||||||
m.DefaultUser.Permissions[name] = value
|
|
||||||
|
|
||||||
for _, u := range m.Users {
|
|
||||||
// Bypass the user if it is already defined.
|
|
||||||
if _, ok := u.Permissions[name]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Permissions == nil {
|
|
||||||
u.Permissions = m.DefaultUser.Permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Admin {
|
|
||||||
u.Permissions[name] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
err := m.db.Save(u)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
|
||||||
// Compatible with http.Handler.
|
|
||||||
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
code, err := serveHTTP(&RequestContext{
|
|
||||||
FM: m,
|
|
||||||
User: nil,
|
|
||||||
FI: nil,
|
|
||||||
}, w, r)
|
|
||||||
|
|
||||||
if code != 0 {
|
|
||||||
w.WriteHeader(code)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
} else {
|
|
||||||
log.Print(code)
|
|
||||||
w.Write([]byte(http.StatusText(code)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allowed checks if the user has permission to access a directory/file.
|
|
||||||
func (u User) Allowed(url string) bool {
|
|
||||||
var rule *Rule
|
|
||||||
i := len(u.Rules) - 1
|
|
||||||
|
|
||||||
for i >= 0 {
|
|
||||||
rule = u.Rules[i]
|
|
||||||
|
|
||||||
if rule.Regex {
|
|
||||||
if rule.Regexp.MatchString(url) {
|
|
||||||
return rule.Allow
|
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(url, rule.Path) {
|
|
||||||
return rule.Allow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchString checks if this string matches the regular expression.
|
|
||||||
func (r *Regexp) MatchString(s string) bool {
|
|
||||||
if r.regexp == nil {
|
|
||||||
r.regexp = regexp.MustCompile(r.Raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.regexp.MatchString(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runner runs the commands for a certain event type.
|
// Runner runs the commands for a certain event type.
|
||||||
func (m FileManager) Runner(event string, path string) error {
|
func (m FileManager) Runner(event string, path string, destination string, user *User) error {
|
||||||
commands := []string{}
|
commands := []string{}
|
||||||
|
|
||||||
// Get the commands from the File Manager instance itself.
|
// Get the commands from the File Manager instance itself.
|
||||||
@@ -503,7 +302,15 @@ func (m FileManager) Runner(event string, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(command, args...)
|
cmd := exec.Command(command, args...)
|
||||||
cmd.Env = append(os.Environ(), "file="+path)
|
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("ROOT=%s", string(user.Scope)))
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", event))
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
|
||||||
|
|
||||||
|
if destination != "" {
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", destination))
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
@@ -525,3 +332,217 @@ func (m FileManager) Runner(event string, path string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultUser is used on New, when no 'base' user is provided.
|
||||||
|
var DefaultUser = User{
|
||||||
|
AllowCommands: true,
|
||||||
|
AllowEdit: true,
|
||||||
|
AllowNew: true,
|
||||||
|
AllowPublish: true,
|
||||||
|
LockPassword: false,
|
||||||
|
Commands: []string{},
|
||||||
|
Rules: []*Rule{},
|
||||||
|
CSS: "",
|
||||||
|
Admin: true,
|
||||||
|
Locale: "en",
|
||||||
|
Scope: ".",
|
||||||
|
FileSystem: fileutils.Dir("."),
|
||||||
|
ViewMode: "mosaic",
|
||||||
|
}
|
||||||
|
|
||||||
|
// User contains the configuration for each user.
|
||||||
|
type User struct {
|
||||||
|
// ID is the required primary key with auto increment0
|
||||||
|
ID int `storm:"id,increment"`
|
||||||
|
|
||||||
|
// Username is the user username used to login.
|
||||||
|
Username string `json:"username" storm:"index,unique"`
|
||||||
|
|
||||||
|
// The hashed password. This never reaches the front-end because it's temporarily
|
||||||
|
// emptied during JSON marshall.
|
||||||
|
Password string `json:"password"`
|
||||||
|
|
||||||
|
// Tells if this user is an admin.
|
||||||
|
Admin bool `json:"admin"`
|
||||||
|
|
||||||
|
// Scope is the path the user has access to.
|
||||||
|
Scope string `json:"filesystem"`
|
||||||
|
|
||||||
|
// FileSystem is the virtual file system the user has access.
|
||||||
|
FileSystem FileSystem `json:"-"`
|
||||||
|
|
||||||
|
// Rules is an array of access and deny rules.
|
||||||
|
Rules []*Rule `json:"rules"`
|
||||||
|
|
||||||
|
// Custom styles for this user.
|
||||||
|
CSS string `json:"css"`
|
||||||
|
|
||||||
|
// Locale is the language of the user.
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
|
||||||
|
// Prevents the user to change its password.
|
||||||
|
LockPassword bool `json:"lockPassword"`
|
||||||
|
|
||||||
|
// These indicate if the user can perform certain actions.
|
||||||
|
AllowNew bool `json:"allowNew"` // Create files and folders
|
||||||
|
AllowEdit bool `json:"allowEdit"` // Edit/rename files
|
||||||
|
AllowCommands bool `json:"allowCommands"` // Execute commands
|
||||||
|
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
|
||||||
|
|
||||||
|
// Commands is the list of commands the user can execute.
|
||||||
|
Commands []string `json:"commands"`
|
||||||
|
|
||||||
|
// User view mode for files and folders.
|
||||||
|
ViewMode string `json:"viewMode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowed checks if the user has permission to access a directory/file.
|
||||||
|
func (u User) Allowed(url string) bool {
|
||||||
|
var rule *Rule
|
||||||
|
i := len(u.Rules) - 1
|
||||||
|
|
||||||
|
for i >= 0 {
|
||||||
|
rule = u.Rules[i]
|
||||||
|
|
||||||
|
if rule.Regex {
|
||||||
|
if rule.Regexp.MatchString(url) {
|
||||||
|
return rule.Allow
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(url, rule.Path) {
|
||||||
|
return rule.Allow
|
||||||
|
}
|
||||||
|
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule is a dissalow/allow rule.
|
||||||
|
type Rule struct {
|
||||||
|
// Regex indicates if this rule uses Regular Expressions or not.
|
||||||
|
Regex bool `json:"regex"`
|
||||||
|
|
||||||
|
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
|
||||||
|
Allow bool `json:"allow"`
|
||||||
|
|
||||||
|
// Path is the corresponding URL path for this rule.
|
||||||
|
Path string `json:"path"`
|
||||||
|
|
||||||
|
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
|
||||||
|
Regexp *Regexp `json:"regexp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regexp is a regular expression wrapper around native regexp.
|
||||||
|
type Regexp struct {
|
||||||
|
Raw string `json:"raw"`
|
||||||
|
regexp *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchString checks if this string matches the regular expression.
|
||||||
|
func (r *Regexp) MatchString(s string) bool {
|
||||||
|
if r.regexp == nil {
|
||||||
|
r.regexp = regexp.MustCompile(r.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.regexp.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShareLink is the information needed to build a shareable link.
|
||||||
|
type ShareLink struct {
|
||||||
|
Hash string `json:"hash" storm:"id,index"`
|
||||||
|
Path string `json:"path" storm:"index"`
|
||||||
|
Expires bool `json:"expires"`
|
||||||
|
ExpireDate time.Time `json:"expireDate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store is a collection of the stores needed to get
|
||||||
|
// and save information.
|
||||||
|
type Store struct {
|
||||||
|
Users UsersStore
|
||||||
|
Config ConfigStore
|
||||||
|
Share ShareStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsersStore is the interface to manage users.
|
||||||
|
type UsersStore interface {
|
||||||
|
Get(id int, builder FSBuilder) (*User, error)
|
||||||
|
GetByUsername(username string, builder FSBuilder) (*User, error)
|
||||||
|
Gets(builder FSBuilder) ([]*User, error)
|
||||||
|
Save(u *User) error
|
||||||
|
Update(u *User, fields ...string) error
|
||||||
|
Delete(id int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigStore is the interface to manage configuration.
|
||||||
|
type ConfigStore interface {
|
||||||
|
Get(name string, to interface{}) error
|
||||||
|
Save(name string, from interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShareStore is the interface to manage share links.
|
||||||
|
type ShareStore interface {
|
||||||
|
Get(hash string) (*ShareLink, error)
|
||||||
|
GetPermanent(path string) (*ShareLink, error)
|
||||||
|
GetByPath(path string) ([]*ShareLink, error)
|
||||||
|
Gets() ([]*ShareLink, error)
|
||||||
|
Save(s *ShareLink) error
|
||||||
|
Delete(hash string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// StaticGen is a static website generator.
|
||||||
|
type StaticGen interface {
|
||||||
|
SettingsPath() string
|
||||||
|
Name() string
|
||||||
|
Setup() error
|
||||||
|
|
||||||
|
Hook(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
|
||||||
|
Preview(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
|
||||||
|
Publish(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileSystem is the interface to work with the file system.
|
||||||
|
type FileSystem interface {
|
||||||
|
Mkdir(name string, perm os.FileMode) error
|
||||||
|
OpenFile(name string, flag int, perm os.FileMode) (*os.File, error)
|
||||||
|
RemoveAll(name string) error
|
||||||
|
Rename(oldName, newName string) error
|
||||||
|
Stat(name string) (os.FileInfo, error)
|
||||||
|
Copy(src, dst string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context contains the needed information to make handlers work.
|
||||||
|
type Context struct {
|
||||||
|
*FileManager
|
||||||
|
User *User
|
||||||
|
File *File
|
||||||
|
// On API handlers, Router is the APi handler we want.
|
||||||
|
Router string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPassword generates an hash from a password using bcrypt.
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPasswordHash compares a password with an hash to check if they match.
|
||||||
|
func CheckPasswordHash(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRandomBytes returns securely generated random bytes.
|
||||||
|
// It will return an fm.Error if the system's secure random
|
||||||
|
// number generator fails to function correctly, in which
|
||||||
|
// case the caller should not continue.
|
||||||
|
func GenerateRandomBytes(n int) ([]byte, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
// Note that err == nil only if we read len(b) bytes.
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package filemanager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hacdias/fileutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type test struct {
|
|
||||||
*FileManager
|
|
||||||
Temp string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t test) Clean() {
|
|
||||||
t.db.Close()
|
|
||||||
os.RemoveAll(t.Temp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTest(t *testing.T) *test {
|
|
||||||
temp, err := ioutil.TempDir("", t.Name())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error creating temporary directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
scope := filepath.Join(temp, "scope")
|
|
||||||
database := filepath.Join(temp, "database.db")
|
|
||||||
|
|
||||||
err = fileutils.CopyDir("./testdata", scope)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error copying the test data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
user := DefaultUser
|
|
||||||
user.FileSystem = fileutils.Dir(scope)
|
|
||||||
|
|
||||||
fm, err := New(database, user)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error creating a file manager instance: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &test{
|
|
||||||
FileManager: fm,
|
|
||||||
Temp: temp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
257
http.go
257
http.go
@@ -1,257 +0,0 @@
|
|||||||
package filemanager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RequestContext contains the needed information to make handlers work.
|
|
||||||
type RequestContext struct {
|
|
||||||
User *User
|
|
||||||
FM *FileManager
|
|
||||||
FI *file
|
|
||||||
// On API handlers, Router is the APi handler we want.
|
|
||||||
Router string
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveHTTP is the main entry point of this HTML application.
|
|
||||||
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
|
|
||||||
// returns a 404 error because we're not supposed to be here!
|
|
||||||
p := strings.TrimPrefix(r.URL.Path, c.FM.BaseURL)
|
|
||||||
|
|
||||||
if len(p) >= len(r.URL.Path) && c.FM.BaseURL != "" {
|
|
||||||
return http.StatusNotFound, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r.URL.Path = p
|
|
||||||
|
|
||||||
// Check if this request is made to the service worker. If so,
|
|
||||||
// pass it through a template to add the needed variables.
|
|
||||||
if r.URL.Path == "/sw.js" {
|
|
||||||
return renderFile(
|
|
||||||
w,
|
|
||||||
c.FM.assets.MustString("sw.js"),
|
|
||||||
"application/javascript",
|
|
||||||
c,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if this request is made to the static assets folder. If so, and
|
|
||||||
// if it is a GET request, returns with the asset. Otherwise, returns
|
|
||||||
// a status not implemented.
|
|
||||||
if matchURL(r.URL.Path, "/static") {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
return http.StatusNotImplemented, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return staticHandler(c, w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if this request is made to the API and directs to the
|
|
||||||
// API handler if so.
|
|
||||||
if matchURL(r.URL.Path, "/api") {
|
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
|
|
||||||
return apiHandler(c, w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any other request should show the index.html file.
|
|
||||||
w.Header().Set("x-frame-options", "SAMEORIGIN")
|
|
||||||
w.Header().Set("x-content-type", "nosniff")
|
|
||||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
|
||||||
|
|
||||||
return renderFile(
|
|
||||||
w,
|
|
||||||
c.FM.assets.MustString("index.html"),
|
|
||||||
"text/html",
|
|
||||||
c,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// staticHandler handles the static assets path.
|
|
||||||
func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
if r.URL.Path != "/static/manifest.json" {
|
|
||||||
http.FileServer(c.FM.assets.HTTPBox()).ServeHTTP(w, r)
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderFile(
|
|
||||||
w,
|
|
||||||
c.FM.assets.MustString("static/manifest.json"),
|
|
||||||
"application/json",
|
|
||||||
c,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// apiHandler is the main entry point for the /api endpoint.
|
|
||||||
func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
if r.URL.Path == "/auth/get" {
|
|
||||||
return authHandler(c, w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.URL.Path == "/auth/renew" {
|
|
||||||
return renewAuthHandler(c, w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
valid, _ := validateAuth(c, r)
|
|
||||||
if !valid {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Router, r.URL.Path = splitURL(r.URL.Path)
|
|
||||||
|
|
||||||
if !c.User.Allowed(r.URL.Path) {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for p := range c.FM.Plugins {
|
|
||||||
code, err := plugins[p].Handler.Before(c, w, r)
|
|
||||||
if code != 0 || err != nil {
|
|
||||||
return code, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Router == "checksum" || c.Router == "download" {
|
|
||||||
var err error
|
|
||||||
c.FI, err = getInfo(r.URL, c.FM, c.User)
|
|
||||||
if err != nil {
|
|
||||||
return errorToHTTP(err, false), err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var code int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch c.Router {
|
|
||||||
case "download":
|
|
||||||
code, err = downloadHandler(c, w, r)
|
|
||||||
case "checksum":
|
|
||||||
code, err = checksumHandler(c, w, r)
|
|
||||||
case "command":
|
|
||||||
code, err = command(c, w, r)
|
|
||||||
case "search":
|
|
||||||
code, err = search(c, w, r)
|
|
||||||
case "resource":
|
|
||||||
code, err = resourceHandler(c, w, r)
|
|
||||||
case "users":
|
|
||||||
code, err = usersHandler(c, w, r)
|
|
||||||
case "commands":
|
|
||||||
code, err = commandsHandler(c, w, r)
|
|
||||||
case "plugins":
|
|
||||||
code, err = pluginsHandler(c, w, r)
|
|
||||||
default:
|
|
||||||
code = http.StatusNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
if code >= 300 || err != nil {
|
|
||||||
return code, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for p := range c.FM.Plugins {
|
|
||||||
code, err := plugins[p].Handler.After(c, w, r)
|
|
||||||
if code != 0 || err != nil {
|
|
||||||
return code, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return code, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
|
|
||||||
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
query := r.URL.Query().Get("algo")
|
|
||||||
|
|
||||||
val, err := c.FI.Checksum(query)
|
|
||||||
if err == errInvalidOption {
|
|
||||||
return http.StatusBadRequest, err
|
|
||||||
} else if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(val))
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitURL splits the path and returns everything that stands
|
|
||||||
// before the first slash and everything that goes after.
|
|
||||||
func splitURL(path string) (string, string) {
|
|
||||||
if path == "" {
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
path = strings.TrimPrefix(path, "/")
|
|
||||||
|
|
||||||
i := strings.Index(path, "/")
|
|
||||||
if i == -1 {
|
|
||||||
return "", path
|
|
||||||
}
|
|
||||||
|
|
||||||
return path[0:i], path[i:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderFile renders a file using a template with some needed variables.
|
|
||||||
func renderFile(w http.ResponseWriter, file string, contentType string, c *RequestContext) (int, error) {
|
|
||||||
tpl := template.Must(template.New("file").Parse(file))
|
|
||||||
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
|
||||||
|
|
||||||
var javascript = ""
|
|
||||||
for name := range c.FM.Plugins {
|
|
||||||
javascript += plugins[name].JavaScript + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tpl.Execute(w, map[string]interface{}{
|
|
||||||
"BaseURL": c.FM.RootURL(),
|
|
||||||
"JavaScript": template.JS(javascript),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderJSON prints the JSON version of data to the browser.
|
|
||||||
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
|
|
||||||
marsh, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
if _, err := w.Write(marsh); err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchURL checks if the first URL matches the second.
|
|
||||||
func matchURL(first, second string) bool {
|
|
||||||
first = strings.ToLower(first)
|
|
||||||
second = strings.ToLower(second)
|
|
||||||
|
|
||||||
return strings.HasPrefix(first, second)
|
|
||||||
}
|
|
||||||
|
|
||||||
// errorToHTTP converts errors to HTTP Status Code.
|
|
||||||
func errorToHTTP(err error, gone bool) int {
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return http.StatusOK
|
|
||||||
case os.IsPermission(err):
|
|
||||||
return http.StatusForbidden
|
|
||||||
case os.IsNotExist(err):
|
|
||||||
if !gone {
|
|
||||||
return http.StatusNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.StatusGone
|
|
||||||
case os.IsExist(err):
|
|
||||||
return http.StatusConflict
|
|
||||||
default:
|
|
||||||
return http.StatusInternalServerError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
|
|
||||||
jwt "github.com/dgrijalva/jwt-go"
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
"github.com/dgrijalva/jwt-go/request"
|
"github.com/dgrijalva/jwt-go/request"
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
// authHandler proccesses the authentication for the user.
|
// authHandler proccesses the authentication for the user.
|
||||||
func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
// NoAuth instances shouldn't call this method.
|
||||||
|
if c.NoAuth {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Receive the credentials from the request and unmarshal them.
|
// Receive the credentials from the request and unmarshal them.
|
||||||
var cred User
|
var cred fm.User
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
@@ -27,13 +30,13 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the user exists.
|
// Checks if the user exists.
|
||||||
u, ok := c.FM.Users[cred.Username]
|
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the password is correct.
|
// Checks if the password is correct.
|
||||||
if !checkPasswordHash(cred.Password, u.Password) {
|
if !fm.CheckPasswordHash(cred.Password, u.Password) {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
|
|||||||
|
|
||||||
// renewAuthHandler is used when the front-end already has a JWT token
|
// renewAuthHandler is used when the front-end already has a JWT token
|
||||||
// and is checking if it is up to date. If so, updates its info.
|
// and is checking if it is up to date. If so, updates its info.
|
||||||
func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func renewAuthHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
ok, u := validateAuth(c, r)
|
ok, u := validateAuth(c, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
@@ -55,15 +58,15 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// claims is the JWT claims.
|
// claims is the JWT claims.
|
||||||
type claims struct {
|
type claims struct {
|
||||||
User
|
fm.User
|
||||||
jwt.StandardClaims
|
jwt.StandardClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
// printToken prints the final JWT token to the user.
|
// printToken prints the final JWT token to the user.
|
||||||
func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
|
func printToken(c *fm.Context, w http.ResponseWriter) (int, error) {
|
||||||
// Creates a copy of the user and removes it password
|
// Creates a copy of the user and removes it password
|
||||||
// hash so it never arrives to the user.
|
// hash so it never arrives to the user.
|
||||||
u := User{}
|
u := fm.User{}
|
||||||
u = *c.User
|
u = *c.User
|
||||||
u.Password = ""
|
u.Password = ""
|
||||||
|
|
||||||
@@ -78,7 +81,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
|
|||||||
|
|
||||||
// Creates the token and signs it.
|
// Creates the token and signs it.
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
string, err := token.SignedString(c.FM.key)
|
signed, err := token.SignedString(c.Key)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
@@ -86,7 +89,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
|
|||||||
|
|
||||||
// Writes the token.
|
// Writes the token.
|
||||||
w.Header().Set("Content-Type", "cty")
|
w.Header().Set("Content-Type", "cty")
|
||||||
w.Write([]byte(string))
|
w.Write([]byte(signed))
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +115,14 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
|
|||||||
|
|
||||||
// validateAuth is used to validate the authentication and returns the
|
// validateAuth is used to validate the authentication and returns the
|
||||||
// User if it is valid.
|
// User if it is valid.
|
||||||
func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
|
func validateAuth(c *fm.Context, r *http.Request) (bool, *fm.User) {
|
||||||
|
if c.NoAuth {
|
||||||
|
c.User = c.DefaultUser
|
||||||
|
return true, c.User
|
||||||
|
}
|
||||||
|
|
||||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||||
return c.FM.key, nil
|
return c.Key, nil
|
||||||
}
|
}
|
||||||
var claims claims
|
var claims claims
|
||||||
token, err := request.ParseFromRequestWithClaims(r,
|
token, err := request.ParseFromRequestWithClaims(r,
|
||||||
@@ -127,38 +135,11 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
u, ok := c.FM.Users[claims.User.Username]
|
u, err := c.Store.Users.Get(claims.User.ID, c.NewFS)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.User = u
|
c.User = u
|
||||||
return true, u
|
return true, u
|
||||||
}
|
}
|
||||||
|
|
||||||
// hashPassword generates an hash from a password using bcrypt.
|
|
||||||
func hashPassword(password string) (string, error) {
|
|
||||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
||||||
return string(bytes), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkPasswordHash compares a password with an hash to check if they match.
|
|
||||||
func checkPasswordHash(password, hash string) bool {
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateRandomBytes returns securely generated random bytes.
|
|
||||||
// It will return an error if the system's secure random
|
|
||||||
// number generator fails to function correctly, in which
|
|
||||||
// case the caller should not continue.
|
|
||||||
func generateRandomBytes(n int) ([]byte, error) {
|
|
||||||
b := make([]byte, n)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
// Note that err == nil only if we read len(b) bytes.
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -9,25 +9,26 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
"github.com/hacdias/fileutils"
|
"github.com/hacdias/fileutils"
|
||||||
"github.com/mholt/archiver"
|
"github.com/mholt/archiver"
|
||||||
)
|
)
|
||||||
|
|
||||||
// downloadHandler creates an archive in one of the supported formats (zip, tar,
|
// downloadHandler creates an archive in one of the supported formats (zip, tar,
|
||||||
// tar.gz or tar.bz2) and sends it to be downloaded.
|
// tar.gz or tar.bz2) and sends it to be downloaded.
|
||||||
func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func downloadHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
query := r.URL.Query().Get("format")
|
query := r.URL.Query().Get("format")
|
||||||
|
|
||||||
// If the file isn't a directory, serve it using http.ServeFile. We display it
|
// If the file isn't a directory, serve it using http.ServeFile. We display it
|
||||||
// inline if it is requested.
|
// inline if it is requested.
|
||||||
if !c.FI.IsDir {
|
if !c.File.IsDir {
|
||||||
if r.URL.Query().Get("inline") == "true" {
|
if r.URL.Query().Get("inline") == "true" {
|
||||||
w.Header().Set("Content-Disposition", "inline")
|
w.Header().Set("Content-Disposition", "inline")
|
||||||
} else {
|
} else {
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.FI.Name)
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+c.File.Name+"\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
http.ServeFile(w, r, c.FI.Path)
|
http.ServeFile(w, r, c.File.Path)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,14 +47,14 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// Clean the slashes.
|
// Clean the slashes.
|
||||||
name = fileutils.SlashClean(name)
|
name = fileutils.SlashClean(name)
|
||||||
files = append(files, filepath.Join(c.FI.Path, name))
|
files = append(files, filepath.Join(c.File.Path, name))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
files = append(files, c.FI.Path)
|
files = append(files, c.File.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the format is true, just set it to "zip".
|
// If the format is true, just set it to "zip".
|
||||||
if query == "true" {
|
if query == "true" || query == "" {
|
||||||
query = "zip"
|
query = "zip"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Defines the file name.
|
// Defines the file name.
|
||||||
name := c.FI.Name
|
name := c.File.Name
|
||||||
if name == "." || name == "" {
|
if name == "." || name == "" {
|
||||||
name = "download"
|
name = "download"
|
||||||
}
|
}
|
||||||
@@ -106,7 +107,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+name)
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
|
||||||
_, err = io.Copy(w, file)
|
_, err = io.Copy(w, file)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
343
http/http.go
Normal file
343
http/http.go
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler returns a function compatible with http.HandleFunc.
|
||||||
|
func Handler(m *fm.FileManager) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
code, err := serve(&fm.Context{
|
||||||
|
FileManager: m,
|
||||||
|
User: nil,
|
||||||
|
File: nil,
|
||||||
|
}, w, r)
|
||||||
|
|
||||||
|
if code >= 400 {
|
||||||
|
w.WriteHeader(code)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
txt := http.StatusText(code)
|
||||||
|
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
|
||||||
|
w.Write([]byte(txt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve is the main entry point of this HTML application.
|
||||||
|
func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
|
||||||
|
// returns a 404 fm.Error because we're not supposed to be here!
|
||||||
|
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
|
||||||
|
|
||||||
|
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
|
||||||
|
return http.StatusNotFound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.URL.Path = p
|
||||||
|
|
||||||
|
// Check if this request is made to the service worker. If so,
|
||||||
|
// pass it through a template to add the needed variables.
|
||||||
|
if r.URL.Path == "/sw.js" {
|
||||||
|
return renderFile(c, w, "sw.js")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if this request is made to the static assets folder. If so, and
|
||||||
|
// if it is a GET request, returns with the asset. Otherwise, returns
|
||||||
|
// a status not implemented.
|
||||||
|
if matchURL(r.URL.Path, "/static") {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
return http.StatusNotImplemented, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return staticHandler(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if this request is made to the API and directs to the
|
||||||
|
// API handler if so.
|
||||||
|
if matchURL(r.URL.Path, "/api") {
|
||||||
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
|
||||||
|
return apiHandler(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is a request to the preview and a static website generator is
|
||||||
|
// active, build the preview.
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil {
|
||||||
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview")
|
||||||
|
return c.StaticGen.Preview(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/share/") {
|
||||||
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
|
||||||
|
return sharePage(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other request should show the index.html file.
|
||||||
|
w.Header().Set("x-frame-options", "SAMEORIGIN")
|
||||||
|
w.Header().Set("x-content-type", "nosniff")
|
||||||
|
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||||
|
|
||||||
|
return renderFile(c, w, "index.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
// staticHandler handles the static assets path.
|
||||||
|
func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
if r.URL.Path != "/static/manifest.json" {
|
||||||
|
http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderFile(c, w, "static/manifest.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiHandler is the main entry point for the /api endpoint.
|
||||||
|
func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
if r.URL.Path == "/auth/get" {
|
||||||
|
return authHandler(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == "/auth/renew" {
|
||||||
|
return renewAuthHandler(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
valid, _ := validateAuth(c, r)
|
||||||
|
if !valid {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Router, r.URL.Path = splitURL(r.URL.Path)
|
||||||
|
|
||||||
|
if !c.User.Allowed(r.URL.Path) {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.StaticGen != nil {
|
||||||
|
// If we are using the 'magic url' for the settings,
|
||||||
|
// we should redirect the request for the acutual path.
|
||||||
|
if r.URL.Path == "/settings" {
|
||||||
|
r.URL.Path = c.StaticGen.SettingsPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executes the Static website generator hook.
|
||||||
|
code, err := c.StaticGen.Hook(c, w, r)
|
||||||
|
if code != 0 || err != nil {
|
||||||
|
return code, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Router == "checksum" || c.Router == "download" {
|
||||||
|
var err error
|
||||||
|
c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorToHTTP(err, false), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var code int
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch c.Router {
|
||||||
|
case "download":
|
||||||
|
code, err = downloadHandler(c, w, r)
|
||||||
|
case "checksum":
|
||||||
|
code, err = checksumHandler(c, w, r)
|
||||||
|
case "command":
|
||||||
|
code, err = command(c, w, r)
|
||||||
|
case "search":
|
||||||
|
code, err = search(c, w, r)
|
||||||
|
case "resource":
|
||||||
|
code, err = resourceHandler(c, w, r)
|
||||||
|
case "users":
|
||||||
|
code, err = usersHandler(c, w, r)
|
||||||
|
case "settings":
|
||||||
|
code, err = settingsHandler(c, w, r)
|
||||||
|
case "share":
|
||||||
|
code, err = shareHandler(c, w, r)
|
||||||
|
default:
|
||||||
|
code = http.StatusNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
|
||||||
|
func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
query := r.URL.Query().Get("algo")
|
||||||
|
|
||||||
|
val, err := c.File.Checksum(query)
|
||||||
|
if err == fm.ErrInvalidOption {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
} else if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(val))
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitURL splits the path and returns everything that stands
|
||||||
|
// before the first slash and everything that goes after.
|
||||||
|
func splitURL(path string) (string, string) {
|
||||||
|
if path == "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
|
||||||
|
i := strings.Index(path, "/")
|
||||||
|
if i == -1 {
|
||||||
|
return "", path
|
||||||
|
}
|
||||||
|
|
||||||
|
return path[0:i], path[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderFile renders a file using a template with some needed variables.
|
||||||
|
func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) {
|
||||||
|
tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file)))
|
||||||
|
|
||||||
|
var contentType string
|
||||||
|
switch filepath.Ext(file) {
|
||||||
|
case ".html":
|
||||||
|
contentType = "text/html"
|
||||||
|
case ".js":
|
||||||
|
contentType = "application/javascript"
|
||||||
|
case ".json":
|
||||||
|
contentType = "application/json"
|
||||||
|
default:
|
||||||
|
contentType = "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"BaseURL": c.RootURL(),
|
||||||
|
"NoAuth": c.NoAuth,
|
||||||
|
"Version": fm.Version,
|
||||||
|
"CSS": template.CSS(c.CSS),
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.StaticGen != nil {
|
||||||
|
data["StaticGen"] = c.StaticGen.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tpl.Execute(w, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sharePage build the share page.
|
||||||
|
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
s, err := c.Store.Share.Get(r.URL.Path)
|
||||||
|
if err == fm.ErrNotExist {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return renderFile(c, w, "static/share/404.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Expires && s.ExpireDate.Before(time.Now()) {
|
||||||
|
c.Store.Share.Delete(s.Hash)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return renderFile(c, w, "static/share/404.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.URL.Path = s.Path
|
||||||
|
|
||||||
|
info, err := os.Stat(s.Path)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorToHTTP(err, false), err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.File = &fm.File{
|
||||||
|
Path: s.Path,
|
||||||
|
Name: info.Name(),
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
Mode: info.Mode(),
|
||||||
|
IsDir: info.IsDir(),
|
||||||
|
Size: info.Size(),
|
||||||
|
}
|
||||||
|
|
||||||
|
dl := r.URL.Query().Get("dl")
|
||||||
|
|
||||||
|
if dl == "" || dl == "0" {
|
||||||
|
tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html")))
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
|
err := tpl.Execute(w, map[string]interface{}{
|
||||||
|
"BaseURL": c.RootURL(),
|
||||||
|
"File": c.File,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadHandler(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderJSON prints the JSON version of data to the browser.
|
||||||
|
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
|
||||||
|
marsh, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
if _, err := w.Write(marsh); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchURL checks if the first URL matches the second.
|
||||||
|
func matchURL(first, second string) bool {
|
||||||
|
first = strings.ToLower(first)
|
||||||
|
second = strings.ToLower(second)
|
||||||
|
|
||||||
|
return strings.HasPrefix(first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorToHTTP converts errors to HTTP Status Code.
|
||||||
|
func ErrorToHTTP(err error, gone bool) int {
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return http.StatusOK
|
||||||
|
case os.IsPermission(err):
|
||||||
|
return http.StatusForbidden
|
||||||
|
case os.IsNotExist(err):
|
||||||
|
if !gone {
|
||||||
|
return http.StatusNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusGone
|
||||||
|
case os.IsExist(err):
|
||||||
|
return http.StatusConflict
|
||||||
|
default:
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
"github.com/hacdias/fileutils"
|
"github.com/hacdias/fileutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +27,7 @@ func sanitizeURL(url string) string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
r.URL.Path = sanitizeURL(r.URL.Path)
|
r.URL.Path = sanitizeURL(r.URL.Path)
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
@@ -33,8 +37,8 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
return resourceDeleteHandler(c, w, r)
|
return resourceDeleteHandler(c, w, r)
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
// Before save command handler.
|
// Before save command handler.
|
||||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||||
if err := c.FM.Runner("before_save", path); err != nil {
|
if err := c.Runner("before_save", path, "", c.User); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +48,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// After save command handler.
|
// After save command handler.
|
||||||
if err := c.FM.Runner("after_save", path); err != nil {
|
if err := c.Runner("after_save", path, "", c.User); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,11 +62,11 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
return http.StatusNotImplemented, nil
|
return http.StatusNotImplemented, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// Gets the information of the directory/file.
|
// Gets the information of the directory/file.
|
||||||
f, err := getInfo(r.URL, c.FM, c.User)
|
f, err := fm.GetInfo(r.URL, c.FileManager, c.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorToHTTP(err, false), err
|
return ErrorToHTTP(err, false), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a dir and the path doesn't end with a trailing slash,
|
// If it's a dir and the path doesn't end with a trailing slash,
|
||||||
@@ -73,13 +77,13 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
// If it is a dir, go and serve the listing.
|
// If it is a dir, go and serve the listing.
|
||||||
if f.IsDir {
|
if f.IsDir {
|
||||||
c.FI = f
|
c.File = f
|
||||||
return listingHandler(c, w, r)
|
return listingHandler(c, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tries to get the file type.
|
// Tries to get the file type.
|
||||||
if err = f.GetFileType(true); err != nil {
|
if err = f.GetFileType(true); err != nil {
|
||||||
return errorToHTTP(err, true), err
|
return ErrorToHTTP(err, true), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve a preview if the file can't be edited or the
|
// Serve a preview if the file can't be edited or the
|
||||||
@@ -93,26 +97,26 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
f.Kind = "editor"
|
f.Kind = "editor"
|
||||||
|
|
||||||
// Tries to get the editor data.
|
// Tries to get the editor data.
|
||||||
if err = f.getEditor(); err != nil {
|
if err = f.GetEditor(); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderJSON(w, f)
|
return renderJSON(w, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
f := c.FI
|
f := c.File
|
||||||
f.Kind = "listing"
|
f.Kind = "listing"
|
||||||
|
|
||||||
// Tries to get the listing data.
|
// Tries to get the listing data.
|
||||||
if err := f.getListing(c, r); err != nil {
|
if err := f.GetListing(c.User, r); err != nil {
|
||||||
return errorToHTTP(err, true), err
|
return ErrorToHTTP(err, true), err
|
||||||
}
|
}
|
||||||
|
|
||||||
listing := f.listing
|
listing := f.Listing
|
||||||
|
|
||||||
// Defines the cookie scope.
|
// Defines the cookie scope.
|
||||||
cookieScope := c.FM.RootURL()
|
cookieScope := c.RootURL()
|
||||||
if cookieScope == "" {
|
if cookieScope == "" {
|
||||||
cookieScope = "/"
|
cookieScope = "/"
|
||||||
}
|
}
|
||||||
@@ -126,27 +130,35 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
listing.ApplySort()
|
listing.ApplySort()
|
||||||
listing.Display = displayMode(w, r, cookieScope)
|
|
||||||
|
|
||||||
return renderJSON(w, f)
|
return renderJSON(w, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// Prevent the removal of the root directory.
|
// Prevent the removal of the root directory.
|
||||||
if r.URL.Path == "/" || !c.User.AllowEdit {
|
if r.URL.Path == "/" || !c.User.AllowEdit {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire the before trigger.
|
||||||
|
if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the file or folder.
|
// Remove the file or folder.
|
||||||
err := c.User.FileSystem.RemoveAll(r.URL.Path)
|
err := c.User.FileSystem.RemoveAll(r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorToHTTP(err, true), err
|
return ErrorToHTTP(err, true), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire the after trigger.
|
||||||
|
if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if !c.User.AllowNew && r.Method == http.MethodPost {
|
if !c.User.AllowNew && r.Method == http.MethodPost {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
@@ -155,6 +167,12 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
|
|||||||
return http.StatusForbidden, nil
|
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.
|
// Checks if the current request is for a directory and not a file.
|
||||||
if strings.HasSuffix(r.URL.Path, "/") {
|
if strings.HasSuffix(r.URL.Path, "/") {
|
||||||
// If the method is PUT, we return 405 Method not Allowed, because
|
// If the method is PUT, we return 405 Method not Allowed, because
|
||||||
@@ -164,46 +182,118 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we try to create the directory.
|
// 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
|
return ErrorToHTTP(err, false), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If using POST method, we are trying to create a new file so it is not
|
// 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
|
// desirable to override an already existent file. Thus, we check
|
||||||
// if the file already exists. If so, we just return a 409 Conflict.
|
// 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 {
|
if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil {
|
||||||
return http.StatusConflict, errors.New("There is already a file on that path")
|
return http.StatusConflict, errors.New("There is already a file on that path")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire the before trigger.
|
||||||
|
if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
// Create/Open the file.
|
// 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 {
|
if err != nil {
|
||||||
return errorToHTTP(err, false), err
|
return ErrorToHTTP(err, false), err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
// Copies the new content for the file.
|
// Copies the new content for the file.
|
||||||
_, err = io.Copy(f, r.Body)
|
_, err = io.Copy(f, r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorToHTTP(err, false), err
|
return ErrorToHTTP(err, false), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the info about the file.
|
// Gets the info about the file.
|
||||||
fi, err := f.Stat()
|
fi, err := f.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorToHTTP(err, false), err
|
return ErrorToHTTP(err, false), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this instance has a Static Generator and handles publishing
|
||||||
|
// or scheduling if it's the case.
|
||||||
|
if c.StaticGen != nil {
|
||||||
|
code, err := resourcePublishSchedule(c, w, r)
|
||||||
|
if code != 0 {
|
||||||
|
return code, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes the ETag Header.
|
// Writes the ETag Header.
|
||||||
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
|
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
|
||||||
w.Header().Set("ETag", etag)
|
w.Header().Set("ETag", etag)
|
||||||
|
|
||||||
|
// Fire the after trigger.
|
||||||
|
if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
publish := r.Header.Get("Publish")
|
||||||
|
schedule := r.Header.Get("Schedule")
|
||||||
|
|
||||||
|
if publish != "true" && schedule == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.User.AllowPublish {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if publish == "true" {
|
||||||
|
return resourcePublish(c, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.Parse("2006-01-02T15:04", schedule)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
|
||||||
|
_, err := resourcePublish(c, w, r)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||||
|
|
||||||
|
// Before save command handler.
|
||||||
|
if err := c.Runner("before_publish", path, "", c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := c.StaticGen.Publish(c, w, r)
|
||||||
|
if err != nil {
|
||||||
|
return code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executed the before publish command.
|
||||||
|
if err := c.Runner("before_publish", path, "", c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
|
||||||
// resourcePatchHandler is the entry point for resource handler.
|
// resourcePatchHandler is the entry point for resource handler.
|
||||||
func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if !c.User.AllowEdit {
|
if !c.User.AllowEdit {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
@@ -212,7 +302,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
|
|||||||
action := r.Header.Get("Action")
|
action := r.Header.Get("Action")
|
||||||
dst, err := url.QueryUnescape(dst)
|
dst, err := url.QueryUnescape(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorToHTTP(err, true), err
|
return ErrorToHTTP(err, true), err
|
||||||
}
|
}
|
||||||
|
|
||||||
src := r.URL.Path
|
src := r.URL.Path
|
||||||
@@ -222,38 +312,34 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if action == "copy" {
|
if action == "copy" {
|
||||||
|
// Fire the after trigger.
|
||||||
|
if err := c.Runner("before_copy", src, dst, c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file.
|
||||||
err = c.User.FileSystem.Copy(src, dst)
|
err = c.User.FileSystem.Copy(src, dst)
|
||||||
|
|
||||||
|
// Fire the after trigger.
|
||||||
|
if err := c.Runner("after_copy", src, dst, c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Fire the after trigger.
|
||||||
|
if err := c.Runner("before_rename", src, dst, c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the file.
|
||||||
err = c.User.FileSystem.Rename(src, dst)
|
err = c.User.FileSystem.Rename(src, dst)
|
||||||
|
|
||||||
|
// Fire the after trigger.
|
||||||
|
if err := c.Runner("after_rename", src, dst, c.User); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorToHTTP(err, true), err
|
return ErrorToHTTP(err, true), err
|
||||||
}
|
|
||||||
|
|
||||||
// displayMode obtains the display mode from the Cookie.
|
|
||||||
func displayMode(w http.ResponseWriter, r *http.Request, scope string) string {
|
|
||||||
var displayMode string
|
|
||||||
|
|
||||||
// Checks the cookie.
|
|
||||||
if displayCookie, err := r.Cookie("display"); err == nil {
|
|
||||||
displayMode = displayCookie.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's invalid, set it to mosaic, which is the default.
|
|
||||||
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
|
|
||||||
displayMode = "mosaic"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the cookie.
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "display",
|
|
||||||
Value: displayMode,
|
|
||||||
MaxAge: 31536000,
|
|
||||||
Path: scope,
|
|
||||||
Secure: r.TLS != nil,
|
|
||||||
})
|
|
||||||
|
|
||||||
return displayMode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
|
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user