Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ed1b41d6 | ||
|
|
bb9b0dfd2b | ||
|
|
cc2ce884fc | ||
|
|
e7e7679002 | ||
|
|
9a829fd594 | ||
|
|
624d61930c | ||
|
|
ee30e7711f | ||
|
|
6e5116aa27 | ||
|
|
34acffbb7b | ||
|
|
eb6f26c191 | ||
|
|
06f3e9744a | ||
|
|
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 |
@@ -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,
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@@ -7,16 +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 build -ldflags "-X main.version=$(git tag -l --points-at HEAD)"
|
RUN CGO_ENABLED=0 go build -a
|
||||||
RUN mv filemanager /go/bin/filemanager
|
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"]
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
<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="staticgen" content="{{ .StaticGen }}">
|
||||||
|
<meta name="noauth" content="{{ .NoAuth }}">
|
||||||
|
<meta name="version" content="{{ .Version }}">
|
||||||
|
<meta name="recaptcha" content="{{ .ReCaptchaKey }}">
|
||||||
<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">
|
||||||
@@ -22,7 +25,14 @@
|
|||||||
<!-- Add to home screen for Windows -->
|
<!-- Add to home screen for Windows -->
|
||||||
<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">
|
||||||
<% for (var chunk of webpack.compilation.chunks) {
|
|
||||||
|
<script>CSS = "{{ .CSS }}"</script>
|
||||||
|
|
||||||
|
{{ if .ReCaptcha -}}
|
||||||
|
<script src='https://www.google.com/recaptcha/api.js?render=explicit'></script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<% for (var chunk of webpack.chunks) {
|
||||||
for (var file of chunk.files) {
|
for (var file of chunk.files) {
|
||||||
if (file.match(/\.(js|css)$/)) { %>
|
if (file.match(/\.(js|css)$/)) { %>
|
||||||
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
||||||
|
|||||||
@@ -1,18 +1,74 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view></router-view>
|
<router-view :dependencies="loaded" @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
mounted: function () {
|
computed: mapState(['recaptcha']),
|
||||||
// Remove loading animation.
|
data () {
|
||||||
let loading = document.getElementById('loading')
|
return {
|
||||||
loading.classList.add('done')
|
loaded: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.recaptcha.length === 0) {
|
||||||
|
this.unload()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(function () {
|
let check = () => {
|
||||||
loading.parentNode.removeChild(loading)
|
if (typeof window.grecaptcha === 'undefined') {
|
||||||
}, 200)
|
setTimeout(check, 100)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unload()
|
||||||
|
}
|
||||||
|
|
||||||
|
check()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unload () {
|
||||||
|
this.loaded = true
|
||||||
|
// Remove loading animation.
|
||||||
|
let loading = document.getElementById('loading')
|
||||||
|
loading.classList.add('done')
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
loading.parentNode.removeChild(loading)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
this.updateCSS()
|
||||||
|
},
|
||||||
|
updateCSS (global = false) {
|
||||||
|
let css = this.$store.state.css
|
||||||
|
|
||||||
|
if (typeof this.$store.state.user.css === 'string' && !global) {
|
||||||
|
css += '\n' + this.$store.state.user.css
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeCSS()
|
||||||
|
|
||||||
|
let style = document.createElement('style')
|
||||||
|
style.title = 'custom-css'
|
||||||
|
style.type = 'text/css'
|
||||||
|
style.appendChild(document.createTextNode(css))
|
||||||
|
document.head.appendChild(style)
|
||||||
|
},
|
||||||
|
removeCSS () {
|
||||||
|
let style = document.querySelector('style[title="custom-css"]')
|
||||||
|
if (style === undefined || style === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
style.parentElement.removeChild(style)
|
||||||
|
},
|
||||||
|
cleanCSS () {
|
||||||
|
this.updateCSS(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<select v-on:change="change" :value="selected">
|
<select v-on:change="change" :value="selected">
|
||||||
<option value="en">{{ $t('languages.en') }}</option>
|
<option value="en">{{ $t('languages.en') }}</option>
|
||||||
|
<option value="fr">{{ $t('languages.fr') }}</option>
|
||||||
<option value="pt">{{ $t('languages.pt') }}</option>
|
<option value="pt">{{ $t('languages.pt') }}</option>
|
||||||
<option value="ja">{{ $t('languages.ja') }}</option>
|
<option value="ja">{{ $t('languages.ja') }}</option>
|
||||||
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!$store.state.user.noAuth">
|
<div v-if="!$store.state.noAuth">
|
||||||
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
||||||
<i class="material-icons">settings_applications</i>
|
<i class="material-icons">settings_applications</i>
|
||||||
<span>{{ $t('sidebar.settings') }}</span>
|
<span>{{ $t('sidebar.settings') }}</span>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="credits">
|
<p class="credits">
|
||||||
<span>{{ $t('sidebar.servedWith') }} <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><a @click="help">{{ $t('sidebar.help') }}</a></span>
|
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -72,7 +72,7 @@ import auth from '@/utils/auth'
|
|||||||
export default {
|
export default {
|
||||||
name: 'sidebar',
|
name: 'sidebar',
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['user', 'staticGen']),
|
...mapState(['user', 'staticGen', 'version']),
|
||||||
active () {
|
active () {
|
||||||
return this.$store.state.show === 'sidebar'
|
return this.$store.state.show === 'sidebar'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" 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>{{ $t('buttons.switchView') }}</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'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done(button)
|
buttons.done(button)
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
this.$store.commit('setSchedule', '')
|
this.$store.commit('setSchedule', '')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" 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"
|
||||||
@dragenter="dragEnter"
|
@dragenter="dragEnter"
|
||||||
@dragend="dragEnd">
|
@dragend="dragEnd">
|
||||||
<div>
|
<div>
|
||||||
@@ -98,7 +98,7 @@ 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')
|
||||||
},
|
},
|
||||||
@@ -210,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.
|
||||||
@@ -267,7 +263,7 @@ export default {
|
|||||||
.then(req => {
|
.then(req => {
|
||||||
this.checkConflict(files, req.items, base)
|
this.checkConflict(files, req.items, base)
|
||||||
})
|
})
|
||||||
.catch(error => { console.log(error) })
|
.catch(this.$showError)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -318,19 +314,37 @@ export default {
|
|||||||
handleFiles (files, base, overwrite = false) {
|
handleFiles (files, base, overwrite = false) {
|
||||||
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, overwrite))
|
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.success('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
|
||||||
|
|||||||
@@ -109,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(() => {
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
<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!
|
||||||
@@ -75,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,19 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.copy') }}</h3>
|
<div class="card-title">
|
||||||
<p>{{ $t('prompts.copyMessage') }}</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"
|
<button class="cancel flat"
|
||||||
@click="copy"
|
|
||||||
:aria-label="$t('buttons.copy')"
|
|
||||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
|
||||||
<button class="cancel"
|
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
: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>
|
||||||
@@ -56,7 +60,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('copy')
|
buttons.done('copy')
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.deleteTitle') }}</h3>
|
<div class="card-content">
|
||||||
<p v-show="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
||||||
<p v-show="req.kind === 'listing'">{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
||||||
<div>
|
</div>
|
||||||
<button @click="submit"
|
<div class="card-action">
|
||||||
:aria-label="$t('buttons.delete')"
|
<button @click="$store.commit('closeHovers')"
|
||||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
class="flat cancel"
|
||||||
<button class="cancel"
|
|
||||||
@click="$store.commit('closeHovers')"
|
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
: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>
|
||||||
@@ -43,7 +45,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('delete')
|
buttons.done('delete')
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -70,7 +72,7 @@ export default {
|
|||||||
.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,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt" id="download">
|
<div class="card floating" id="download">
|
||||||
<h3>{{ $t('prompts.download') }}</h3>
|
<div class="card-title">
|
||||||
<p>{{ $t('prompts.downloadMessage') }}</p>
|
<h2>{{ $t('prompts.download') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button @click="download('zip')" autofocus>zip</button>
|
<div class="card-content">
|
||||||
<button @click="download('tar')" autofocus>tar</button>
|
<p>{{ $t('prompts.downloadMessage') }}</p>
|
||||||
<button @click="download('targz')" autofocus>tar.gz</button>
|
|
||||||
<button @click="download('tarbz2')" autofocus>tar.bz2</button>
|
<button class="block cancel" @click="download('zip')" autofocus>zip</button>
|
||||||
<button @click="download('tarxz')" autofocus>tar.xz</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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="prompt error">
|
|
||||||
<i class="material-icons">error_outline</i>
|
|
||||||
<h3>{{ $t('prompts.error') }}</h3>
|
|
||||||
<pre>{{ $store.state.showMessage }}</pre>
|
|
||||||
<div>
|
|
||||||
<button @click="close"
|
|
||||||
autofocus
|
|
||||||
:aria-label="$t('buttons.close')"
|
|
||||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
|
||||||
<button @click="reportIssue"
|
|
||||||
class="cancel"
|
|
||||||
:aria-label="$t('buttons.reportIssue')"
|
|
||||||
:title="$t('buttons.reportIssue')">{{ $t('buttons.reportIssue') }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'error',
|
|
||||||
methods: {
|
|
||||||
reportIssue () {
|
|
||||||
window.open('https://github.com/hacdias/filemanager/issues/new')
|
|
||||||
},
|
|
||||||
close () {
|
|
||||||
this.$store.commit('closeHovers')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -53,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) {
|
||||||
@@ -96,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,23 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt help">
|
<div class="card floating help">
|
||||||
<h3>{{ $t('help.help') }}</h3>
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('help.help') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul>
|
<div class="card-content">
|
||||||
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
<ul>
|
||||||
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
||||||
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
|
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
||||||
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
|
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
|
||||||
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
|
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
|
||||||
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
|
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
|
||||||
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
|
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
|
||||||
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
|
||||||
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
||||||
</ul>
|
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="card-action">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
class="ok"
|
class="flat"
|
||||||
:aria-label="$t('buttons.ok')"
|
:aria-label="$t('buttons.ok')"
|
||||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.fileInfo') }}</h3>
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('prompts.fileInfo') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p v-show="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</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>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
|
<p v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name() }}</p>
|
||||||
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
<p><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
||||||
<p v-show="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime() }}</p>
|
<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>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
|
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
|
||||||
<p><strong>{{ $t('prompts.numberDirs') }}:</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')">{{ $t('prompts.show') }}</a></code></p>
|
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
|
||||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
||||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
|
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
|
||||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
|
<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"
|
<button type="submit"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
class="ok"
|
class="flat"
|
||||||
:aria-label="$t('buttons.ok')"
|
:aria-label="$t('buttons.ok')"
|
||||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,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,19 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.move') }}</h3>
|
<div class="card-title">
|
||||||
<p>{{ $t('prompts.moveMessage') }}</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"
|
<button class="flat cancel"
|
||||||
@click="move"
|
|
||||||
:aria-label="$t('buttons.move')"
|
|
||||||
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
|
||||||
<button class="cancel"
|
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
: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>
|
||||||
@@ -56,7 +59,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('move')
|
buttons.done('move')
|
||||||
this.$store.commit('showError', error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
<div class="card-title">
|
||||||
<p>{{ $t('prompts.newArchetype') }}</p>
|
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
</div>
|
||||||
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
|
|
||||||
<div>
|
<div class="card-content">
|
||||||
<button class="ok"
|
<p>{{ $t('prompts.newArchetype') }}</p>
|
||||||
@click="submit"
|
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||||
:aria-label="$t('buttons.create')"
|
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
|
||||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
</div>
|
||||||
<button class="cancel"
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="flat cancel"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
: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>
|
||||||
@@ -37,9 +43,7 @@ export default {
|
|||||||
.then((url) => {
|
.then((url) => {
|
||||||
this.$router.push({ path: url })
|
this.$router.push({ path: url })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(this.$showError)
|
||||||
this.$store.commit('showError', error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
new (url, type) {
|
new (url, type) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url)
|
||||||
@@ -47,7 +51,7 @@ export default {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
|
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
if (!this.$store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
||||||
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.newDir') }}</h3>
|
<div class="card-title">
|
||||||
<p>{{ $t('prompts.newDirMessage') }}</p>
|
<h2>{{ $t('prompts.newDir') }}</h2>
|
||||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
</div>
|
||||||
<div>
|
|
||||||
<button class="ok"
|
<div class="card-content">
|
||||||
:aria-label="$t('buttons.create')"
|
<p>{{ $t('prompts.newDirMessage') }}</p>
|
||||||
:title="$t('buttons.create')"
|
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||||
@click="submit">{{ $t('buttons.create') }}</button>
|
</div>
|
||||||
<button class="cancel"
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="cancel flat"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
: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>
|
||||||
@@ -43,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,17 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
<div class="card-title">
|
||||||
<p>{{ $t('prompts.newFileMessage') }}</p>
|
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
</div>
|
||||||
<div>
|
|
||||||
<button class="ok"
|
<div class="card-content">
|
||||||
@click="submit"
|
<p>{{ $t('prompts.newFileMessage') }}</p>
|
||||||
:aria-label="$t('buttons.create')"
|
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
</div>
|
||||||
<button class="cancel"
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="cancel flat"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
: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>
|
||||||
@@ -44,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,8 +9,6 @@
|
|||||||
<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>
|
|
||||||
<success v-else-if="showSuccess"></success>
|
|
||||||
<replace v-else-if="showReplace"></replace>
|
<replace v-else-if="showReplace"></replace>
|
||||||
<schedule v-else-if="show === 'schedule'"></schedule>
|
<schedule v-else-if="show === 'schedule'"></schedule>
|
||||||
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
|
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
|
||||||
@@ -27,8 +25,6 @@ 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 NewArchetype from './NewArchetype'
|
||||||
@@ -47,9 +43,7 @@ export default {
|
|||||||
NewArchetype,
|
NewArchetype,
|
||||||
Schedule,
|
Schedule,
|
||||||
Rename,
|
Rename,
|
||||||
Error,
|
|
||||||
Download,
|
Download,
|
||||||
Success,
|
|
||||||
Move,
|
Move,
|
||||||
Copy,
|
Copy,
|
||||||
Share,
|
Share,
|
||||||
@@ -70,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' },
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.rename') }}</h3>
|
<div class="card-title">
|
||||||
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
<h2>{{ $t('prompts.rename') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
<div class="card-content">
|
||||||
<div>
|
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
||||||
<button @click="submit"
|
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||||
type="submit"
|
</div>
|
||||||
:aria-label="$t('buttons.rename')"
|
|
||||||
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
<div class="card-action">
|
||||||
<button class="cancel"
|
<button class="cancel flat"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
: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>
|
||||||
@@ -68,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')
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.replace') }}</h3>
|
<div class="card-title">
|
||||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
<h2>{{ $t('prompts.replace') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="card-content">
|
||||||
<button class="ok"
|
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||||
@click="showConfirm"
|
</div>
|
||||||
:aria-label="$t('buttons.replace')"
|
|
||||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
<div class="card-action">
|
||||||
<button class="cancel"
|
<button class="flat cancel"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt">
|
<div class="card floating">
|
||||||
<h3>{{ $t('prompts.schedule') }}</h3>
|
<div class="card-title">
|
||||||
<p>{{ $t('prompts.scheduleMessage') }}</p>
|
<h2>{{ $t('prompts.schedule') }}</h2>
|
||||||
<input autofocus type="datetime-local" v-model="date">
|
</div>
|
||||||
<div>
|
|
||||||
<button class="ok"
|
<div class="card-content">
|
||||||
@click="submit"
|
<p>{{ $t('prompts.scheduleMessage') }}</p>
|
||||||
:aria-label="$t('buttons.schedule')"
|
<input autofocus type="datetime-local" v-model="date">
|
||||||
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
|
</div>
|
||||||
<button class="cancel"
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="cancel flat"
|
||||||
@click="close"
|
@click="close"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,51 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="prompt" id="share">
|
<div class="card floating" id="share">
|
||||||
<h3>{{ $t('buttons.share') }}</h3>
|
<div class="card-title">
|
||||||
<p></p>
|
<h2>{{ $t('buttons.share') }}</h2>
|
||||||
<ul>
|
</div>
|
||||||
<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">
|
<div class="card-content">
|
||||||
<a :href="buildLink(link.hash)" target="_blank">
|
<ul>
|
||||||
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
|
<li v-if="!hasPermanent">
|
||||||
<template v-else>{{ $t('permanent') }}</template>
|
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||||
</a>
|
</li>
|
||||||
|
|
||||||
<button class="action"
|
<li v-for="link in links" :key="link.hash">
|
||||||
@click="deleteLink($event, link)"
|
<a :href="buildLink(link.hash)" target="_blank">
|
||||||
:aria-label="$t('buttons.delete')"
|
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
|
||||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
<template v-else>{{ $t('permanent') }}</template>
|
||||||
|
</a>
|
||||||
|
|
||||||
<button class="action copy"
|
<button class="action"
|
||||||
:data-clipboard-text="buildLink(link.hash)"
|
@click="deleteLink($event, link)"
|
||||||
:aria-label="$t('buttons.copyToClipboard')"
|
:aria-label="$t('buttons.delete')"
|
||||||
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
<button class="action copy-clipboard"
|
||||||
<input autofocus
|
:data-clipboard-text="buildLink(link.hash)"
|
||||||
type="number"
|
:aria-label="$t('buttons.copyToClipboard')"
|
||||||
max="2147483647"
|
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||||
min="0"
|
</li>
|
||||||
@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>
|
<li>
|
||||||
<button class="cancel"
|
<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')"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.close')"
|
:aria-label="$t('buttons.close')"
|
||||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||||
@@ -54,7 +58,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapMutations } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { getShare, deleteShare, share } from '@/utils/api'
|
import { getShare, deleteShare, share } from '@/utils/api'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import Clipboard from 'clipboard'
|
import Clipboard from 'clipboard'
|
||||||
@@ -101,20 +105,25 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error === 404) return
|
if (error === 404) return
|
||||||
this.showError(error)
|
this.$showError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.clip = new Clipboard('.copy')
|
this.clip = new Clipboard('.copy-clipboard')
|
||||||
|
this.clip.on('success', (e) => {
|
||||||
|
this.$showSuccess(this.$t('success.linkCopied'))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
this.clip.destroy()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations([ 'showError' ]),
|
|
||||||
submit: function (event) {
|
submit: function (event) {
|
||||||
if (!this.time) return
|
if (!this.time) return
|
||||||
|
|
||||||
share(this.url, this.time, this.unit)
|
share(this.url, this.time, this.unit)
|
||||||
.then(result => { this.links.push(result); this.sort() })
|
.then(result => { this.links.push(result); this.sort() })
|
||||||
.catch(error => { this.showError(error) })
|
.catch(this.$showError)
|
||||||
},
|
},
|
||||||
getPermalink (event) {
|
getPermalink (event) {
|
||||||
share(this.url)
|
share(this.url)
|
||||||
@@ -123,7 +132,7 @@ export default {
|
|||||||
this.sort()
|
this.sort()
|
||||||
this.hasPermanent = true
|
this.hasPermanent = true
|
||||||
})
|
})
|
||||||
.catch(error => { this.showError(error) })
|
.catch(this.$showError)
|
||||||
},
|
},
|
||||||
deleteLink (event, link) {
|
deleteLink (event, link) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -132,7 +141,7 @@ export default {
|
|||||||
if (!link.expires) this.hasPermanent = false
|
if (!link.expires) this.hasPermanent = false
|
||||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||||
})
|
})
|
||||||
.catch(error => { this.showError(error) })
|
.catch(this.$showError)
|
||||||
},
|
},
|
||||||
humanTime (time) {
|
humanTime (time) {
|
||||||
return moment(time).fromNow()
|
return moment(time).fromNow()
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="prompt success">
|
|
||||||
<i class="material-icons">done</i>
|
|
||||||
<h3>{{ $store.state.showMessage }}</h3>
|
|
||||||
<div>
|
|
||||||
<button @click="close"
|
|
||||||
:aria-label="$t('buttons.ok')"
|
|
||||||
:title="$t('buttons.ok')"
|
|
||||||
autofocus>{{ $t('buttons.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,64 +1,34 @@
|
|||||||
.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 > h1:first-of-type {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard form > p:last-child,
|
|
||||||
form.dashboard > p:last-child {
|
|
||||||
text-align: right
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard > *:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard select,
|
|
||||||
.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 #locale,
|
||||||
@@ -69,49 +39,32 @@ form.dashboard > p:last-child {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard #locale {
|
.dashboard #locale {
|
||||||
border: 1px solid #dddddd;
|
|
||||||
margin-top: .5em;
|
margin-top: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard textarea:focus,
|
textarea:focus,
|
||||||
.dashboard textarea:hover,
|
textarea:hover,
|
||||||
.dashboard input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
.dashboard input[type="password"]:focus,
|
input[type="password"]:focus,
|
||||||
.dashboard input[type="text"]:hover,
|
input[type="text"]:hover,
|
||||||
.dashboard input[type="password"]: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,
|
||||||
@@ -131,21 +84,337 @@ p code {
|
|||||||
display: flex;
|
display: flex;
|
||||||
color: rgb(84, 110, 122);
|
color: rgb(84, 110, 122);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 0 0 1em;
|
|
||||||
margin: 0 0 1em;
|
margin: 0 0 1em;
|
||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
text-align: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard #nav li {
|
.dashboard #nav li {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0 0 1em;
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard #nav li:last-child {
|
.dashboard #nav li.active {
|
||||||
text-align: right
|
border-color: #2196f3
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard #nav i {
|
.dashboard #nav i {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
vertical-align: middle;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -122,8 +122,9 @@ 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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -29,6 +29,14 @@
|
|||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#login.recaptcha form {
|
||||||
|
min-width: 304px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login #recaptcha {
|
||||||
|
margin: .5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
#login input {
|
#login input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,208 +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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt#share ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt#share ul li {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt#share ul li a {
|
|
||||||
color: #2196F3;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt#share ul li .action i {
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt#share ul li input,
|
|
||||||
.prompt#share ul li select {
|
|
||||||
padding: .2em;
|
|
||||||
margin-right: .5em;
|
|
||||||
border: 1px solid #dadada;
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
@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";
|
||||||
@@ -180,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 *
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ buttons:
|
|||||||
publish: Publish
|
publish: Publish
|
||||||
selectMultiple: Select multiple
|
selectMultiple: Select multiple
|
||||||
schedule: Schedule
|
schedule: Schedule
|
||||||
switchView: Swicth view
|
switchView: Switch view
|
||||||
toggleSidebar: Toggle sidebar
|
toggleSidebar: Toggle sidebar
|
||||||
update: Update
|
update: Update
|
||||||
upload: Upload
|
upload: Upload
|
||||||
permalink: Get Permanent Link
|
permalink: Get Permanent Link
|
||||||
|
success:
|
||||||
|
linkCopied: Link copied!
|
||||||
errors:
|
errors:
|
||||||
forbidden: You're not welcome here.
|
forbidden: You're not welcome here.
|
||||||
internal: Something really went wrong.
|
internal: Something really went wrong.
|
||||||
@@ -115,13 +117,14 @@ settings:
|
|||||||
commandsHelp: >
|
commandsHelp: >
|
||||||
Here you can set commands that are executed in the named events. You
|
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
|
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
|
after saving, the environment variable "FILE" will be available with the path
|
||||||
of the file.
|
of the file.
|
||||||
commandsUpdated: Commands updated!
|
commandsUpdated: Commands updated!
|
||||||
customStylesheet: Custom Stylesheet
|
customStylesheet: Custom Stylesheet
|
||||||
examples: Examples
|
examples: Examples
|
||||||
globalSettings: Global Settings
|
globalSettings: Global Settings
|
||||||
language: Language
|
language: Language
|
||||||
|
lockPassword: Prevent the user from changing the password
|
||||||
newPassword: Your new password
|
newPassword: Your new password
|
||||||
newPasswordConfirm: Confirm your new password
|
newPasswordConfirm: Confirm your new password
|
||||||
newUser: New User
|
newUser: New User
|
||||||
@@ -165,7 +168,6 @@ sidebar:
|
|||||||
myFiles: My files
|
myFiles: My files
|
||||||
newFile: New file
|
newFile: New file
|
||||||
newFolder: New folder
|
newFolder: New folder
|
||||||
servedWith: Served with
|
|
||||||
settings: Settings
|
settings: Settings
|
||||||
siteSettings: Site Settings
|
siteSettings: Site Settings
|
||||||
hugoNew: Hugo New
|
hugoNew: Hugo New
|
||||||
@@ -185,6 +187,7 @@ search:
|
|||||||
writeToSearch: Write here to search
|
writeToSearch: Write here to search
|
||||||
languages:
|
languages:
|
||||||
en: English
|
en: English
|
||||||
|
fr: Français
|
||||||
pt: Português
|
pt: Português
|
||||||
ja: 日本語
|
ja: 日本語
|
||||||
zhCN: 中文 (简体)
|
zhCN: 中文 (简体)
|
||||||
|
|||||||
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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueI18n from 'vue-i18n'
|
import VueI18n from 'vue-i18n'
|
||||||
import en from './en.yaml'
|
import en from './en.yaml'
|
||||||
|
import fr from './fr.yaml'
|
||||||
import pt from './pt.yaml'
|
import pt from './pt.yaml'
|
||||||
import ja from './ja.yaml'
|
import ja from './ja.yaml'
|
||||||
import zhCN from './zh-cn.yaml'
|
import zhCN from './zh-cn.yaml'
|
||||||
@@ -13,6 +14,7 @@ const i18n = new VueI18n({
|
|||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
messages: {
|
messages: {
|
||||||
'en': en,
|
'en': en,
|
||||||
|
'fr': fr,
|
||||||
'pt': pt,
|
'pt': pt,
|
||||||
'ja': ja,
|
'ja': ja,
|
||||||
'zh-cn': zhCN,
|
'zh-cn': zhCN,
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ buttons:
|
|||||||
update: 更新
|
update: 更新
|
||||||
upload: アップロード
|
upload: アップロード
|
||||||
permalink: 固定リンク
|
permalink: 固定リンク
|
||||||
|
success:
|
||||||
|
linkCopied: リンクがコピーされました!
|
||||||
errors:
|
errors:
|
||||||
forbidden: アクセスが拒否されました。
|
forbidden: アクセスが拒否されました。
|
||||||
internal: 内部エラーが発生しました。
|
internal: 内部エラーが発生しました。
|
||||||
@@ -116,12 +118,13 @@ settings:
|
|||||||
ここで、名前付きイベントに実行するコマンドを設定することができます。\
|
ここで、名前付きイベントに実行するコマンドを設定することができます。\
|
||||||
一行にコマンド一つを入力してください。\
|
一行にコマンド一つを入力してください。\
|
||||||
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\
|
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\
|
||||||
環境変数 file はファイルのパスに割り当てられます。"
|
環境変数 FILE はファイルのパスに割り当てられます。"
|
||||||
commandsUpdated: コマンドは更新されました!
|
commandsUpdated: コマンドは更新されました!
|
||||||
customStylesheet: カスタムスタイルシ ート
|
customStylesheet: カスタムスタイルシ ート
|
||||||
examples: 例
|
examples: 例
|
||||||
globalSettings: グローバル設定
|
globalSettings: グローバル設定
|
||||||
language: 言語
|
language: 言語
|
||||||
|
lockPassword: 新しいパスワードを変更に禁止
|
||||||
newPassword: 新しいパスワード
|
newPassword: 新しいパスワード
|
||||||
newPasswordConfirm: 新しいパスワードを確認します
|
newPasswordConfirm: 新しいパスワードを確認します
|
||||||
newUser: 新しいユーザー
|
newUser: 新しいユーザー
|
||||||
@@ -165,7 +168,6 @@ sidebar:
|
|||||||
myFiles: 私のファイル
|
myFiles: 私のファイル
|
||||||
newFile: 新しいファイルを作成
|
newFile: 新しいファイルを作成
|
||||||
newFolder: 新しいフォルダを作成
|
newFolder: 新しいフォルダを作成
|
||||||
servedWith: サービス提供者
|
|
||||||
settings: 設定
|
settings: 設定
|
||||||
siteSettings: サイト設定
|
siteSettings: サイト設定
|
||||||
hugoNew: Hugo New
|
hugoNew: Hugo New
|
||||||
@@ -185,6 +187,7 @@ search:
|
|||||||
writeToSearch: ここにキーワードを入力してください
|
writeToSearch: ここにキーワードを入力してください
|
||||||
languages:
|
languages:
|
||||||
en: English
|
en: English
|
||||||
|
fr: Français
|
||||||
pt: Português
|
pt: Português
|
||||||
ja: 日本語
|
ja: 日本語
|
||||||
zhCN: 中文 (简体)
|
zhCN: 中文 (简体)
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ buttons:
|
|||||||
update: Atualizar
|
update: Atualizar
|
||||||
upload: Enviar
|
upload: Enviar
|
||||||
permalink: Obter link permanente
|
permalink: Obter link permanente
|
||||||
|
success:
|
||||||
|
linkCopied: Link copiado!
|
||||||
errors:
|
errors:
|
||||||
forbidden: Tu não és bem-vindo aqui.
|
forbidden: Tu não és bem-vindo aqui.
|
||||||
internal: Algo correu bastante mal.
|
internal: Algo correu bastante mal.
|
||||||
@@ -66,6 +68,7 @@ help:
|
|||||||
help: Ajuda
|
help: Ajuda
|
||||||
languages:
|
languages:
|
||||||
en: English
|
en: English
|
||||||
|
fr: Français
|
||||||
pt: Português
|
pt: Português
|
||||||
ja: 日本語
|
ja: 日本語
|
||||||
zhCN: 中文 (简体)
|
zhCN: 中文 (简体)
|
||||||
@@ -136,12 +139,13 @@ settings:
|
|||||||
Pode definir um conjunto de comandos a executar em determiandos eventos.
|
Pode definir um conjunto de comandos a executar em determiandos eventos.
|
||||||
Deve escrever um comando por linha. Se o evento estiver relacionado com ficheiros,
|
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
|
como antes e depois de guardar, irá existir uma variável de ambiente denominada
|
||||||
"file" com o caminho do ficheiro.
|
"FILE" com o caminho do ficheiro.
|
||||||
commandsUpdated: Comandos atualizados!
|
commandsUpdated: Comandos atualizados!
|
||||||
customStylesheet: Estilos Personalizados
|
customStylesheet: Estilos Personalizados
|
||||||
examples: Exemplos
|
examples: Exemplos
|
||||||
globalSettings: Configurações Globais
|
globalSettings: Configurações Globais
|
||||||
language: Linguagem
|
language: Linguagem
|
||||||
|
lockPassword: Não permitir que o utilizador altere a palavra-passe
|
||||||
newPassword: Nova palavra-passe
|
newPassword: Nova palavra-passe
|
||||||
newPasswordConfirm: Confirme a nova palavra-passe
|
newPasswordConfirm: Confirme a nova palavra-passe
|
||||||
newUser: Novo Utilizador
|
newUser: Novo Utilizador
|
||||||
@@ -189,7 +193,6 @@ sidebar:
|
|||||||
newFile: Novo ficheiro
|
newFile: Novo ficheiro
|
||||||
newFolder: Nova pasta
|
newFolder: Nova pasta
|
||||||
preview: Pré-visualizar
|
preview: Pré-visualizar
|
||||||
servedWith: Servido com
|
|
||||||
settings: Configurações
|
settings: Configurações
|
||||||
siteSettings: Configurações do Site
|
siteSettings: Configurações do Site
|
||||||
time:
|
time:
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ buttons:
|
|||||||
update: 更新
|
update: 更新
|
||||||
upload: 上传
|
upload: 上传
|
||||||
permalink: 获取永久链接
|
permalink: 获取永久链接
|
||||||
|
success:
|
||||||
|
linkCopied: 链接已复制!
|
||||||
errors:
|
errors:
|
||||||
forbidden: 你被禁止访问。
|
forbidden: 你被禁止访问。
|
||||||
internal: 内部出现麻烦了。
|
internal: 内部出现麻烦了。
|
||||||
@@ -115,12 +117,13 @@ settings:
|
|||||||
commandsHelp: "\
|
commandsHelp: "\
|
||||||
在这里,您可以设置在指定事件下执行的命令,一行一条。\
|
在这里,您可以设置在指定事件下执行的命令,一行一条。\
|
||||||
若事件与文件相关,如“在保存文件前”,\
|
若事件与文件相关,如“在保存文件前”,\
|
||||||
则文件的路径会被赋值给环境变量 \"file\"。"
|
则文件的路径会被赋值给环境变量 \"FILE\"。"
|
||||||
commandsUpdated: 命令已更新!
|
commandsUpdated: 命令已更新!
|
||||||
customStylesheet: 自定义样式表
|
customStylesheet: 自定义样式表
|
||||||
examples: 例子
|
examples: 例子
|
||||||
globalSettings: 全局设置
|
globalSettings: 全局设置
|
||||||
language: 语言
|
language: 语言
|
||||||
|
lockPassword: 禁止用户修改密码
|
||||||
newPassword: 您的新密码
|
newPassword: 您的新密码
|
||||||
newPasswordConfirm: 重输一遍新密码
|
newPasswordConfirm: 重输一遍新密码
|
||||||
newUser: 新建用户
|
newUser: 新建用户
|
||||||
@@ -163,7 +166,6 @@ sidebar:
|
|||||||
myFiles: 我的文件
|
myFiles: 我的文件
|
||||||
newFile: 新建文件
|
newFile: 新建文件
|
||||||
newFolder: 新建文件夹
|
newFolder: 新建文件夹
|
||||||
servedWith: '服务提供者:'
|
|
||||||
settings: 设置
|
settings: 设置
|
||||||
siteSettings: 网站设置
|
siteSettings: 网站设置
|
||||||
hugoNew: Hugo New
|
hugoNew: Hugo New
|
||||||
@@ -183,6 +185,7 @@ search:
|
|||||||
writeToSearch: 请输入要搜索的内容
|
writeToSearch: 请输入要搜索的内容
|
||||||
languages:
|
languages:
|
||||||
en: English
|
en: English
|
||||||
|
fr: Français
|
||||||
pt: Português
|
pt: Português
|
||||||
ja: 日本語
|
ja: 日本語
|
||||||
zhCN: 中文 (简体)
|
zhCN: 中文 (简体)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ buttons:
|
|||||||
close: 關閉
|
close: 關閉
|
||||||
copy: 複製
|
copy: 複製
|
||||||
copyFile: 複製檔案
|
copyFile: 複製檔案
|
||||||
copyToClipboard: 複製到剪貼板
|
copyToClipboard: 複製到剪貼簿
|
||||||
create: 建立
|
create: 建立
|
||||||
delete: 刪除
|
delete: 刪除
|
||||||
download: 下載
|
download: 下載
|
||||||
@@ -31,8 +31,10 @@ buttons:
|
|||||||
update: 更新
|
update: 更新
|
||||||
upload: 上傳
|
upload: 上傳
|
||||||
permalink: 獲取永久連結
|
permalink: 獲取永久連結
|
||||||
|
success:
|
||||||
|
linkCopied: 連結已複製!
|
||||||
errors:
|
errors:
|
||||||
forbidden: 你被禁止訪問。
|
forbidden: 你被禁止存取。
|
||||||
internal: 內部出現麻煩了。
|
internal: 內部出現麻煩了。
|
||||||
notFound: 找不到檔案。
|
notFound: 找不到檔案。
|
||||||
files:
|
files:
|
||||||
@@ -45,7 +47,7 @@ files:
|
|||||||
lastModified: 最後修改
|
lastModified: 最後修改
|
||||||
loading: 讀取中...
|
loading: 讀取中...
|
||||||
lonely: 這裡沒有任何檔案...
|
lonely: 這裡沒有任何檔案...
|
||||||
metadata: 中繼資料
|
metadata: 詮釋資料
|
||||||
multipleSelectionEnabled: 多選模式已開啟
|
multipleSelectionEnabled: 多選模式已開啟
|
||||||
name: 名稱
|
name: 名稱
|
||||||
size: 大小
|
size: 大小
|
||||||
@@ -100,70 +102,70 @@ prompts:
|
|||||||
show: 顯示
|
show: 顯示
|
||||||
size: 大小
|
size: 大小
|
||||||
schedule: 計畫
|
schedule: 計畫
|
||||||
scheduleMessage: 請選擇發佈這篇帖子的日期。
|
scheduleMessage: 請選擇發佈這篇貼文的日期。
|
||||||
newArchetype: 建立一個基於原型的新帖子。您的檔案將會建立在內容資料夾中。
|
newArchetype: 建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。
|
||||||
settings:
|
settings:
|
||||||
admin: 管理員
|
admin: 管理員
|
||||||
administrator: 管理員
|
administrator: 管理員
|
||||||
allowCommands: 執行命令
|
allowCommands: 執行命令
|
||||||
allowEdit: 編輯、重命名或刪除檔案/目錄
|
allowEdit: 編輯、重命名或刪除檔案/目錄
|
||||||
allowNew: 創建新檔案和目錄
|
allowNew: 創建新檔案和目錄
|
||||||
allowPublish: 發佈新的帖子與頁面
|
allowPublish: 發佈新的貼文與頁面
|
||||||
avoidChanges: '(留空以避免更改)'
|
avoidChanges: '(留空以避免更改)'
|
||||||
changePassword: 更改密碼
|
changePassword: 更改密碼
|
||||||
commands: 命令
|
commands: 命令
|
||||||
commandsHelp: "\
|
commandsHelp: "\
|
||||||
在這裡,您可以設定在指定事件下執行的命令,一行一條。\
|
在這裡,您可以設定在指定事件下執行的命令,一行一條。\
|
||||||
若事件與檔案相關,如“在保存檔案前”,\
|
若事件與檔案相關,如“在保存檔案前”,\
|
||||||
則檔案的路徑會被賦值給環境變數 \"file\"。"
|
則檔案的路徑會被賦值給環境變數 \"FILE\"。"
|
||||||
commandsUpdated: 命令已更新!
|
commandsUpdated: 命令已更新!
|
||||||
customStylesheet: 自定義樣式表
|
customStylesheet: 自定義樣式表
|
||||||
examples: 例子
|
examples: 範例
|
||||||
globalSettings: 全域設定
|
globalSettings: 全域設定
|
||||||
language: 語言
|
language: 語言
|
||||||
|
lockPassword: 禁止使用者修改密碼
|
||||||
newPassword: 您的新密碼
|
newPassword: 您的新密碼
|
||||||
newPasswordConfirm: 重輸一遍新密碼
|
newPasswordConfirm: 重輸一遍新密碼
|
||||||
newUser: 建立用戶
|
newUser: 建立使用者
|
||||||
password: 密碼
|
password: 密碼
|
||||||
passwordUpdated: 密碼已更新!
|
passwordUpdated: 密碼已更新!
|
||||||
permissions: 權限
|
permissions: 權限
|
||||||
permissionsHelp: "\
|
permissionsHelp: "\
|
||||||
您可以將該用戶設置為管理員,也可以單獨選擇各項權限。\
|
您可以將該使用者設置為管理員,也可以單獨選擇各項權限。\
|
||||||
如果選擇了“管理員”,則其他的選項會被自動勾上,\
|
如果選擇了“管理員”,則其他的選項會被自動勾上,\
|
||||||
同時該用戶可以管理其他用戶。"
|
同時該使用者可以管理其他使用者。"
|
||||||
profileSettings: 設定檔設定
|
profileSettings: 設定檔設定
|
||||||
ruleExample1: "\
|
ruleExample1: "\
|
||||||
封鎖用戶訪問所有資料夾下任何以 . 開頭的檔案\
|
封鎖使用者存取所有資料夾下任何以 . 開頭的檔案\
|
||||||
(隱藏文件, 例如: .git, .gitignore)。"
|
(隱藏文件, 例如: .git, .gitignore)。"
|
||||||
ruleExample2: 封鎖用戶訪問其目錄範圍的根目錄下名為 Caddyfile 的檔案。
|
ruleExample2: 封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。
|
||||||
rules: 規則
|
rules: 規則
|
||||||
rulesHelp1: "\
|
rulesHelp1: "\
|
||||||
您可以為該用戶製定一組黑名單或白名單式的規則,\
|
您可以為該使用者製定一組黑名單或白名單式的規則,\
|
||||||
被屏蔽的檔案將不會顯示在清單中,用戶也無權限訪問,\
|
被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,\
|
||||||
支持相對於目錄範圍的路徑。"
|
支持相對於目錄範圍的路徑。"
|
||||||
rulesHelp2: "\
|
rulesHelp2: "\
|
||||||
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
|
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
|
||||||
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
|
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
|
||||||
scope: 目錄範圍
|
scope: 目錄範圍
|
||||||
settingsUpdated: 設定已更新!
|
settingsUpdated: 設定已更新!
|
||||||
user: 用戶
|
user: 使用者
|
||||||
userCommands: 用戶命令
|
userCommands: 使用者命令
|
||||||
userCommandsHelp: "\
|
userCommandsHelp: "\
|
||||||
指定該用戶可以執行的命令,用空格分隔。\
|
指定該使用者可以執行的命令,用空格分隔。\
|
||||||
例如:"
|
例如:"
|
||||||
userCreated: 用戶已建立!
|
userCreated: 使用者已建立!
|
||||||
userDeleted: 用戶已刪除!
|
userDeleted: 使用者已刪除!
|
||||||
userManagement: 用戶管理
|
userManagement: 使用者管理
|
||||||
username: 用戶名
|
username: 使用者名稱
|
||||||
users: 用戶
|
users: 使用者
|
||||||
userUpdated: 用戶已更新!
|
userUpdated: 使用者已更新!
|
||||||
sidebar:
|
sidebar:
|
||||||
help: 幫助
|
help: 幫助
|
||||||
logout: 登出
|
logout: 登出
|
||||||
myFiles: 我的檔案
|
myFiles: 我的檔案
|
||||||
newFile: 建立檔案
|
newFile: 建立檔案
|
||||||
newFolder: 建立資料夾
|
newFolder: 建立資料夾
|
||||||
servedWith: '服務提供者:'
|
|
||||||
settings: 設定
|
settings: 設定
|
||||||
siteSettings: 網站設定
|
siteSettings: 網站設定
|
||||||
hugoNew: Hugo New
|
hugoNew: Hugo New
|
||||||
@@ -183,12 +185,14 @@ search:
|
|||||||
writeToSearch: 請輸入要搜尋的內容
|
writeToSearch: 請輸入要搜尋的內容
|
||||||
languages:
|
languages:
|
||||||
en: English
|
en: English
|
||||||
|
fr: Français
|
||||||
pt: Português
|
pt: Português
|
||||||
ja: 日本語
|
ja: 日本語
|
||||||
zhCN: 中文 (简体)
|
zhCN: 中文 (简体)
|
||||||
|
zhTW: 中文 (繁體)
|
||||||
time:
|
time:
|
||||||
unit: 時間單位
|
unit: 時間單位
|
||||||
seconds: 秒
|
seconds: 秒
|
||||||
minutes: 分鐘
|
minutes: 分鐘
|
||||||
hours: 小時
|
hours: 小時
|
||||||
days: 天
|
days: 天
|
||||||
|
|||||||
@@ -3,9 +3,46 @@ 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 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'), '', 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',
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import Router from 'vue-router'
|
|||||||
import Login from '@/views/Login'
|
import Login from '@/views/Login'
|
||||||
import Layout from '@/views/Layout'
|
import Layout from '@/views/Layout'
|
||||||
import Files from '@/views/Files'
|
import Files from '@/views/Files'
|
||||||
import Users from '@/views/Users'
|
import Users from '@/views/Settings/Users'
|
||||||
import User from '@/views/User'
|
import User from '@/views/Settings/User'
|
||||||
import GlobalSettings from '@/views/GlobalSettings'
|
import Settings from '@/views/Settings'
|
||||||
import ProfileSettings from '@/views/ProfileSettings'
|
import GlobalSettings from '@/views/settings/Global'
|
||||||
|
import ProfileSettings from '@/views/settings/Profile'
|
||||||
import Error403 from '@/views/errors/403'
|
import Error403 from '@/views/errors/403'
|
||||||
import Error404 from '@/views/errors/404'
|
import Error404 from '@/views/errors/404'
|
||||||
import Error500 from '@/views/errors/500'
|
import Error500 from '@/views/errors/500'
|
||||||
@@ -49,22 +50,41 @@ const router = new Router({
|
|||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
|
component: Settings,
|
||||||
redirect: {
|
redirect: {
|
||||||
path: '/settings/profile'
|
path: '/settings/profile'
|
||||||
}
|
},
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: '/settings/profile',
|
path: '/settings/profile',
|
||||||
name: 'Profile Settings',
|
name: 'Profile Settings',
|
||||||
component: ProfileSettings
|
component: ProfileSettings
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/global',
|
path: '/settings/global',
|
||||||
name: 'Global Settings',
|
name: 'Global Settings',
|
||||||
component: GlobalSettings,
|
component: GlobalSettings,
|
||||||
meta: {
|
meta: {
|
||||||
requiresAdmin: true
|
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',
|
||||||
@@ -81,22 +101,6 @@ const router = new Router({
|
|||||||
name: 'Internal Server Error',
|
name: 'Internal Server Error',
|
||||||
component: Error500
|
component: Error500
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/users',
|
|
||||||
name: 'Users',
|
|
||||||
component: Users,
|
|
||||||
meta: {
|
|
||||||
requiresAdmin: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/users/*',
|
|
||||||
name: 'User',
|
|
||||||
component: User,
|
|
||||||
meta: {
|
|
||||||
requiresAdmin: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/files',
|
path: '/files',
|
||||||
redirect: {
|
redirect: {
|
||||||
|
|||||||
@@ -12,9 +12,18 @@ const state = {
|
|||||||
key: '',
|
key: '',
|
||||||
items: []
|
items: []
|
||||||
},
|
},
|
||||||
|
css: (() => {
|
||||||
|
let css = window.CSS
|
||||||
|
window.CSS = null
|
||||||
|
return css
|
||||||
|
})(),
|
||||||
|
recaptcha: document.querySelector('meta[name="recaptcha"]').getAttribute('content'),
|
||||||
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
|
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: '',
|
schedule: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
reload: false,
|
reload: false,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const mutations = {
|
|||||||
i18n.locale = value.locale
|
i18n.locale = value.locale
|
||||||
state.user = value
|
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)),
|
||||||
@@ -45,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
|
||||||
@@ -61,6 +66,9 @@ const mutations = {
|
|||||||
},
|
},
|
||||||
setSchedule: (state, value) => {
|
setSchedule: (state, value) => {
|
||||||
state.schedule = value
|
state.schedule = value
|
||||||
|
},
|
||||||
|
setProgress: (state, value) => {
|
||||||
|
state.progress = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function fetch (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) {
|
||||||
@@ -41,7 +41,7 @@ export function remove (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) {
|
||||||
@@ -56,13 +56,17 @@ export function remove (url) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function post (url, content = '', overwrite = false) {
|
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) {
|
if (overwrite) {
|
||||||
request.setRequestHeader('Action', `override`)
|
request.setRequestHeader('Action', `override`)
|
||||||
@@ -91,7 +95,7 @@ export function put (url, content = '', publish = false, date = '') {
|
|||||||
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)
|
request.setRequestHeader('Publish', publish)
|
||||||
|
|
||||||
if (date !== '') {
|
if (date !== '') {
|
||||||
@@ -121,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) {
|
||||||
@@ -158,7 +162,7 @@ export function checksum (url, algo) {
|
|||||||
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) {
|
||||||
@@ -222,7 +226,7 @@ export function getSettings () {
|
|||||||
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/settings/`, true)
|
request.open('GET', `${store.state.baseURL}/api/settings/`, 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) {
|
||||||
@@ -251,7 +255,7 @@ export function updateSettings (param, which) {
|
|||||||
|
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
|
request.open('PUT', `${store.state.baseURL}/api/settings/`, 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) {
|
||||||
@@ -274,7 +278,7 @@ 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) {
|
||||||
@@ -295,7 +299,7 @@ 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) {
|
||||||
@@ -316,7 +320,7 @@ 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) {
|
||||||
@@ -341,7 +345,7 @@ 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) {
|
||||||
@@ -366,7 +370,7 @@ 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) {
|
||||||
@@ -391,7 +395,7 @@ export function getShare (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/share${url}`, 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 = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
@@ -410,7 +414,7 @@ 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('DELETE', `${store.state.baseURL}/api/share/${hash}`, 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 = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
@@ -435,7 +439,7 @@ export function share (url, expires = '', unit = 'hours') {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('POST', url, 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 = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ 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) {
|
||||||
@@ -31,8 +31,8 @@ function loggedIn () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function login (user, password) {
|
function login (user, password, captcha) {
|
||||||
let data = {username: user, password: password}
|
let data = {username: user, password: password, recaptcha: captcha}
|
||||||
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/auth/get`, true)
|
request.open('POST', `${store.state.baseURL}/api/auth/get`, true)
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="dashboard">
|
|
||||||
<ul id="nav">
|
|
||||||
<li>
|
|
||||||
<router-link to="/settings/profile">
|
|
||||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.profileSettings') }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link to="/users">
|
|
||||||
{{ $t('settings.userManagement') }} <i class="material-icons">keyboard_arrow_right</i>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h1>{{ $t('settings.globalSettings') }}</h1>
|
|
||||||
|
|
||||||
<form @submit="saveStaticGen" v-if="$store.state.staticGen.length > 0">
|
|
||||||
<h2>{{ capitalize($store.state.staticGen) }}</h2>
|
|
||||||
|
|
||||||
<p v-for="field in staticGen" :key="field.variable">
|
|
||||||
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
|
|
||||||
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
|
|
||||||
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
|
|
||||||
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p><input type="submit" value="Save"></p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form @submit="saveCommands">
|
|
||||||
<h2>{{ $t('settings.commands') }}</h2>
|
|
||||||
|
|
||||||
<p class="small">{{ $t('settings.commandsHelp') }}</p>
|
|
||||||
|
|
||||||
<template v-for="command in commands">
|
|
||||||
<h3>{{ capitalize(command.name) }}</h3>
|
|
||||||
<textarea v-model.trim="command.value"></textarea>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<p><input type="submit" value="Save"></p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState, mapMutations } from 'vuex'
|
|
||||||
import { getSettings, updateSettings } from '@/utils/api'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'settings',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
commands: [],
|
|
||||||
staticGen: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState([ 'user' ])
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
getSettings()
|
|
||||||
.then(settings => {
|
|
||||||
if (this.$store.state.staticGen.length > 0) {
|
|
||||||
this.parseStaticGen(settings.staticGen)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let key in settings.commands) {
|
|
||||||
this.commands.push({
|
|
||||||
name: key,
|
|
||||||
value: settings.commands[key].join('\n')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => { this.showError(error) })
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapMutations([ 'showSuccess', 'showError' ]),
|
|
||||||
capitalize (name, where = '_') {
|
|
||||||
if (where === 'caps') where = /(?=[A-Z])/
|
|
||||||
let splitted = name.split(where)
|
|
||||||
name = ''
|
|
||||||
|
|
||||||
for (let i = 0; i < splitted.length; i++) {
|
|
||||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
|
||||||
}
|
|
||||||
|
|
||||||
return name.slice(0, -1)
|
|
||||||
},
|
|
||||||
saveCommands (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
let commands = {}
|
|
||||||
|
|
||||||
for (let command of this.commands) {
|
|
||||||
let value = command.value.split('\n')
|
|
||||||
if (value.length === 1 && value[0] === '') {
|
|
||||||
value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
commands[command.name] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSettings(commands, 'commands')
|
|
||||||
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
|
|
||||||
.catch(error => { this.showError(error) })
|
|
||||||
},
|
|
||||||
saveStaticGen (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
let staticGen = {}
|
|
||||||
|
|
||||||
for (let field of this.staticGen) {
|
|
||||||
staticGen[field.variable] = field.value
|
|
||||||
|
|
||||||
if (field.original === 'array') {
|
|
||||||
let val = field.value.split(' ')
|
|
||||||
if (val[0] === '') {
|
|
||||||
val.shift()
|
|
||||||
}
|
|
||||||
|
|
||||||
staticGen[field.variable] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSettings(staticGen, 'staticGen')
|
|
||||||
.then(() => { this.showSuccess(this.$t('settings.settingsUpdated')) })
|
|
||||||
.catch(error => { this.showError(error) })
|
|
||||||
},
|
|
||||||
parseStaticGen (staticgen) {
|
|
||||||
for (let option of staticgen) {
|
|
||||||
let value = option.value
|
|
||||||
|
|
||||||
let field = {
|
|
||||||
name: option.name,
|
|
||||||
variable: option.variable,
|
|
||||||
type: 'text',
|
|
||||||
original: 'text',
|
|
||||||
value: value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
field.original = 'array'
|
|
||||||
field.value = value.join(' ')
|
|
||||||
|
|
||||||
this.staticGen.push(field)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (typeof value) {
|
|
||||||
case 'boolean':
|
|
||||||
field.type = 'checkbox'
|
|
||||||
field.original = 'boolean'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
this.staticGen.push(field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<div id="progress">
|
||||||
|
<div v-bind:style="{ width: $store.state.progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
<site-header></site-header>
|
<site-header></site-header>
|
||||||
<sidebar></sidebar>
|
<sidebar></sidebar>
|
||||||
<main>
|
<main>
|
||||||
<router-view v-on:css-updated="updateCSS"></router-view>
|
<router-view @css="$emit('update:css')"></router-view>
|
||||||
</main>
|
</main>
|
||||||
<prompts></prompts>
|
<prompts></prompts>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,23 +34,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.updateCSS()
|
this.$emit('update:css')
|
||||||
},
|
},
|
||||||
methods: {
|
beforeDestroy () {
|
||||||
updateCSS () {
|
this.$emit('clean:css')
|
||||||
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>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="login">
|
<div id="login" :class="{ recaptcha: recaptcha.length > 0 }">
|
||||||
<form @submit="submit">
|
<form @submit="submit">
|
||||||
<img src="../assets/logo.svg" alt="File Manager">
|
<img src="../assets/logo.svg" alt="File Manager">
|
||||||
<h1>File Manager</h1>
|
<h1>File Manager</h1>
|
||||||
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
|
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
|
||||||
<input type="text" v-model="username" :placeholder="$t('login.username')">
|
<input type="text" v-model="username" :placeholder="$t('login.username')">
|
||||||
<input type="password" v-model="password" :placeholder="$t('login.password')">
|
<input type="password" v-model="password" :placeholder="$t('login.password')">
|
||||||
|
<div v-if="recaptcha.length" id="recaptcha"></div>
|
||||||
<input type="submit" :value="$t('login.submit')">
|
<input type="submit" :value="$t('login.submit')">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -13,9 +14,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '@/utils/auth'
|
import auth from '@/utils/auth'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
props: ['dependencies'],
|
||||||
|
computed: mapState(['recaptcha']),
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
wrong: false,
|
wrong: false,
|
||||||
@@ -23,8 +27,23 @@ export default {
|
|||||||
password: ''
|
password: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.dependencies) this.setup()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dependencies: function (val) {
|
||||||
|
if (val) this.setup()
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit: function (event) {
|
setup () {
|
||||||
|
if (this.recaptcha.length === 0) return
|
||||||
|
|
||||||
|
window.grecaptcha.render('recaptcha', {
|
||||||
|
sitekey: this.recaptcha
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submit (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
@@ -33,7 +52,17 @@ export default {
|
|||||||
redirect = '/files/'
|
redirect = '/files/'
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.login(this.username, this.password)
|
let captcha = ''
|
||||||
|
if (this.recaptcha.length > 0) {
|
||||||
|
captcha = window.grecaptcha.getResponse()
|
||||||
|
|
||||||
|
if (captcha === '') {
|
||||||
|
this.wrong = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.login(this.username, this.password, captcha)
|
||||||
.then(() => { this.$router.push({ path: redirect }) })
|
.then(() => { this.$router.push({ path: redirect }) })
|
||||||
.catch(() => { this.wrong = true })
|
.catch(() => { this.wrong = true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="dashboard">
|
|
||||||
<ul id="nav" v-if="user.admin">
|
|
||||||
<li>
|
|
||||||
<router-link to="/settings/global">
|
|
||||||
{{ $t('settings.globalSettings') }} <i class="material-icons">keyboard_arrow_right</i>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h1>{{ $t('settings.profileSettings') }}</h1>
|
|
||||||
|
|
||||||
<form @submit="updateSettings">
|
|
||||||
<h3>{{ $t('settings.language') }}</h3>
|
|
||||||
<p><languages id="locale" :selected.sync="locale"></languages></p>
|
|
||||||
<h3>{{ $t('settings.customStylesheet') }}</h3>
|
|
||||||
<textarea v-model="css" name="css"></textarea>
|
|
||||||
<p><input type="submit" :value="$t('buttons.update')"></p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form @submit="updatePassword">
|
|
||||||
<h3>{{ $t('settings.changePassword') }}</h3>
|
|
||||||
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password"></p>
|
|
||||||
<p><input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p>
|
|
||||||
<p><input type="submit" :value="$t('buttons.update')"></p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState, mapMutations } from 'vuex'
|
|
||||||
import { updateUser } from '@/utils/api'
|
|
||||||
import Languages from '@/components/Languages'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'settings',
|
|
||||||
components: {
|
|
||||||
Languages
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
password: '',
|
|
||||||
passwordConf: '',
|
|
||||||
css: '',
|
|
||||||
locale: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState([ 'user' ]),
|
|
||||||
passwordClass () {
|
|
||||||
if (this.password === '' && this.passwordConf === '') {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.password === this.passwordConf) {
|
|
||||||
return 'green'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'red'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.css = this.user.css
|
|
||||||
this.locale = this.user.locale
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapMutations([ 'showSuccess' ]),
|
|
||||||
updatePassword (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (this.password !== this.passwordConf) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = {
|
|
||||||
ID: this.$store.state.user.ID,
|
|
||||||
password: this.password
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUser(user, 'password').then(location => {
|
|
||||||
this.showSuccess(this.$t('settings.passwordUpdated'))
|
|
||||||
}).catch(e => {
|
|
||||||
this.$store.commit('showError', e)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateSettings (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
let user = {...this.$store.state.user}
|
|
||||||
user.css = this.css
|
|
||||||
user.locale = this.locale
|
|
||||||
|
|
||||||
updateUser(user, 'partial').then(location => {
|
|
||||||
this.$store.commit('setUser', user)
|
|
||||||
this.$emit('css-updated')
|
|
||||||
this.showSuccess(this.$t('settings.settingsUpdated'))
|
|
||||||
}).catch(e => {
|
|
||||||
this.$store.commit('showError', e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
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>
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="dashboard">
|
|
||||||
<ul id="nav">
|
|
||||||
<li>
|
|
||||||
<router-link to="/settings/global">
|
|
||||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.globalSettings') }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<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">
|
|
||||||
<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 * as 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>
|
|
||||||
187
assets/src/views/settings/Global.vue
Normal file
187
assets/src/views/settings/Global.vue
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<form class="card" v-if="staticGen.length" @submit.prevent="saveStaticGen">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ capitalize($store.state.staticGen) }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p v-for="field in staticGen" :key="field.variable">
|
||||||
|
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
|
||||||
|
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
|
||||||
|
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
|
||||||
|
<template v-if="field.type === 'checkbox'">{{ capitalize(field.name, 'caps') }}</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="card" @submit.prevent="saveCSS">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('settings.customStylesheet') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<textarea v-model="css"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="card" @submit.prevent="saveCommands">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('settings.commands') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="small">{{ $t('settings.commandsHelp') }}</p>
|
||||||
|
|
||||||
|
<div v-for="command in commands" :key="command.name" class="collapsible">
|
||||||
|
<input :id="command.name" type="checkbox">
|
||||||
|
<label :for="command.name">
|
||||||
|
<p>{{ capitalize(command.name) }}</p>
|
||||||
|
<i class="material-icons">arrow_drop_down</i>
|
||||||
|
</label>
|
||||||
|
<div class="collapse">
|
||||||
|
<textarea v-model.trim="command.value"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<input class="flat" type="submit" :value="$t('buttons.update')">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import { getSettings, updateSettings } from '@/utils/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'settings',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
commands: [],
|
||||||
|
staticGen: [],
|
||||||
|
css: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState([ 'user' ])
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
getSettings()
|
||||||
|
.then(settings => {
|
||||||
|
if (this.$store.state.staticGen.length > 0) {
|
||||||
|
this.parseStaticGen(settings.staticGen)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in settings.commands) {
|
||||||
|
this.commands.push({
|
||||||
|
name: key,
|
||||||
|
value: settings.commands[key].join('\n')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.css = settings.css
|
||||||
|
})
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
capitalize (name, where = '_') {
|
||||||
|
if (where === 'caps') where = /(?=[A-Z])/
|
||||||
|
let splitted = name.split(where)
|
||||||
|
name = ''
|
||||||
|
|
||||||
|
for (let i = 0; i < splitted.length; i++) {
|
||||||
|
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.slice(0, -1)
|
||||||
|
},
|
||||||
|
saveCommands (event) {
|
||||||
|
let commands = {}
|
||||||
|
|
||||||
|
for (let command of this.commands) {
|
||||||
|
let value = command.value.split('\n')
|
||||||
|
if (value.length === 1 && value[0] === '') {
|
||||||
|
value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
commands[command.name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSettings(commands, 'commands')
|
||||||
|
.then(() => { this.$showSuccess(this.$t('settings.commandsUpdated')) })
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
saveCSS (event) {
|
||||||
|
updateSettings(this.css, 'css')
|
||||||
|
.then(() => {
|
||||||
|
this.$showSuccess(this.$t('settings.settingsUpdated'))
|
||||||
|
this.$store.commit('setCSS', this.css)
|
||||||
|
this.$emit('css')
|
||||||
|
})
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
saveStaticGen (event) {
|
||||||
|
let staticGen = {}
|
||||||
|
|
||||||
|
for (let field of this.staticGen) {
|
||||||
|
staticGen[field.variable] = field.value
|
||||||
|
|
||||||
|
if (field.original === 'array') {
|
||||||
|
let val = field.value.split(' ')
|
||||||
|
if (val[0] === '') {
|
||||||
|
val.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
staticGen[field.variable] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSettings(staticGen, 'staticGen')
|
||||||
|
.then(() => { this.$showSuccess(this.$t('settings.settingsUpdated')) })
|
||||||
|
.catch(this.$showError)
|
||||||
|
},
|
||||||
|
parseStaticGen (staticgen) {
|
||||||
|
for (let option of staticgen) {
|
||||||
|
let value = option.value
|
||||||
|
|
||||||
|
let field = {
|
||||||
|
name: option.name,
|
||||||
|
variable: option.variable,
|
||||||
|
type: 'text',
|
||||||
|
original: 'text',
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
field.original = 'array'
|
||||||
|
field.value = value.join(' ')
|
||||||
|
|
||||||
|
this.staticGen.push(field)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'boolean':
|
||||||
|
field.type = 'checkbox'
|
||||||
|
field.original = 'boolean'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
this.staticGen.push(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
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>
|
||||||
@@ -1,77 +1,81 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<form @submit="save" class="dashboard">
|
<form @submit="save" class="card">
|
||||||
<ul id="nav">
|
<div class="card-title">
|
||||||
<li>
|
<h2 v-if="id === 0">{{ $t('settings.newUser') }}</h2>
|
||||||
<router-link to="/users">
|
<h2 v-else>{{ $t('settings.user') }} {{ username }}</h2>
|
||||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.userManagement') }}
|
</div>
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
|
<div class="card-content">
|
||||||
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
|
|
||||||
|
|
||||||
<p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></p>
|
<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="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="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p>
|
||||||
<p>
|
<p>
|
||||||
<label for="locale">{{ $t('settings.language') }}</label>
|
<label for="locale">{{ $t('settings.language') }}</label>
|
||||||
<languages id="locale" :selected.sync="locale"></languages>
|
<languages id="locale" :selected.sync="locale"></languages>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>{{ $t('settings.permissions') }}</h2>
|
<p><input type="checkbox" :disabled="admin" v-model="lockPassword"> {{ $t('settings.lockPassword') }}</p>
|
||||||
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
|
|
||||||
|
|
||||||
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
|
<h3>{{ $t('settings.permissions') }}</h3>
|
||||||
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
|
<p class="small">{{ $t('settings.permissionsHelp') }}</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><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
|
||||||
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
|
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
|
||||||
<input type="text" v-model.trim="commands">
|
<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>
|
||||||
|
|
||||||
<h2>{{ $t('settings.rules') }}</h2>
|
<h3>{{ $t('settings.userCommands') }}</h3>
|
||||||
|
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
|
||||||
|
<input type="text" v-model.trim="commands">
|
||||||
|
|
||||||
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
|
<h3>{{ $t('settings.rules') }}</h3>
|
||||||
|
|
||||||
<i18n path="settings.rulesHelp2" tag="p" class="small">
|
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
|
||||||
<code>allow</code><code>disallow</code><code>regex</code>
|
|
||||||
</i18n>
|
|
||||||
|
|
||||||
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
|
<i18n path="settings.rulesHelp2" tag="p" class="small">
|
||||||
|
<code>allow</code><code>disallow</code><code>regex</code>
|
||||||
|
</i18n>
|
||||||
|
|
||||||
<ul class="small">
|
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
|
||||||
<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>
|
<ul class="small">
|
||||||
|
<li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
|
||||||
|
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h2>{{ $t('settings.customStylesheet') }}</h2>
|
<textarea v-model.trim="rules"></textarea>
|
||||||
|
|
||||||
<textarea name="css"></textarea>
|
<h3>{{ $t('settings.customStylesheet') }}</h3>
|
||||||
|
|
||||||
<p>
|
<textarea name="css"></textarea>
|
||||||
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
</div>
|
||||||
<input type="submit" :value="$t('buttons.save')">
|
|
||||||
</p>
|
<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>
|
</form>
|
||||||
|
|
||||||
<div v-if="$store.state.show === 'deleteUser'" class="prompt">
|
<div v-if="$store.state.show === 'deleteUser'" class="card floating">
|
||||||
<h3>Delete User</h3>
|
<div class="card-content">
|
||||||
<p>Are you sure you want to delete this user?</p>
|
<p>Are you sure you want to delete this user?</p>
|
||||||
<div>
|
</div>
|
||||||
<button @click="deleteUser" autofocus>{{ $t('buttons.delete') }}</button>
|
|
||||||
<button class="cancel"
|
<div class="card-action">
|
||||||
|
<button class="cancel flat"
|
||||||
@click="closeHovers"
|
@click="closeHovers"
|
||||||
|
autofocus
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">
|
:title="$t('buttons.cancel')">
|
||||||
{{ $t('buttons.cancel') }}
|
{{ $t('buttons.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="flat"
|
||||||
|
@click="deleteUser">
|
||||||
|
{{ $t('buttons.delete') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,6 +97,7 @@ export default {
|
|||||||
allowEdit: false,
|
allowEdit: false,
|
||||||
allowCommands: false,
|
allowCommands: false,
|
||||||
allowPublish: false,
|
allowPublish: false,
|
||||||
|
lockPassword: false,
|
||||||
permissions: {},
|
permissions: {},
|
||||||
password: '',
|
password: '',
|
||||||
username: '',
|
username: '',
|
||||||
@@ -105,7 +110,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
passwordPlaceholder () {
|
passwordPlaceholder () {
|
||||||
if (this.$route.path === '/users/new') return ''
|
if (this.$route.path === '/settings/users/new') return ''
|
||||||
return this.$t('settings.avoidChanges')
|
return this.$t('settings.avoidChanges')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -120,6 +125,7 @@ export default {
|
|||||||
this.allowEdit = true
|
this.allowEdit = true
|
||||||
this.allowNew = true
|
this.allowNew = true
|
||||||
this.allowPublish = true
|
this.allowPublish = true
|
||||||
|
this.lockPassword = false
|
||||||
for (let key in this.permissions) {
|
for (let key in this.permissions) {
|
||||||
this.permissions[key] = true
|
this.permissions[key] = true
|
||||||
}
|
}
|
||||||
@@ -130,7 +136,7 @@ export default {
|
|||||||
fetchData () {
|
fetchData () {
|
||||||
let user = this.$route.params[0]
|
let user = this.$route.params[0]
|
||||||
|
|
||||||
if (this.$route.path === '/users/new') {
|
if (this.$route.path === '/settings/users/new') {
|
||||||
user = 'base'
|
user = 'base'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +147,7 @@ export default {
|
|||||||
this.allowNew = user.allowNew
|
this.allowNew = user.allowNew
|
||||||
this.allowEdit = user.allowEdit
|
this.allowEdit = user.allowEdit
|
||||||
this.allowPublish = user.allowPublish
|
this.allowPublish = user.allowPublish
|
||||||
|
this.lockPassword = user.lockPassword
|
||||||
this.filesystem = user.filesystem
|
this.filesystem = user.filesystem
|
||||||
this.username = user.username
|
this.username = user.username
|
||||||
this.commands = user.commands.join(' ')
|
this.commands = user.commands.join(' ')
|
||||||
@@ -166,7 +173,7 @@ export default {
|
|||||||
|
|
||||||
this.rules = this.rules.trim()
|
this.rules = this.rules.trim()
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$router.push({ path: '/users/new' })
|
this.$router.push({ path: '/settings/users/new' })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
capitalize (name) {
|
capitalize (name) {
|
||||||
@@ -187,6 +194,7 @@ export default {
|
|||||||
this.allowPublish = false
|
this.allowPublish = false
|
||||||
this.permissins = {}
|
this.permissins = {}
|
||||||
this.allowCommands = false
|
this.allowCommands = false
|
||||||
|
this.lockPassword = false
|
||||||
this.password = ''
|
this.password = ''
|
||||||
this.username = ''
|
this.username = ''
|
||||||
this.filesystem = ''
|
this.filesystem = ''
|
||||||
@@ -202,22 +210,22 @@ export default {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
deleteUser(this.id).then(location => {
|
deleteUser(this.id).then(location => {
|
||||||
this.$router.push({ path: '/users' })
|
this.$router.push({ path: '/settings/users' })
|
||||||
this.$store.commit('showSuccess', this.$t('settings.userDeleted'))
|
this.$showSuccess(this.$t('settings.userDeleted'))
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
this.$store.commit('showError', e)
|
this.$showError(e)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save (event) {
|
save (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
let user = this.parseForm()
|
let user = this.parseForm()
|
||||||
|
|
||||||
if (this.$route.path === '/users/new') {
|
if (this.$route.path === '/settings/users/new') {
|
||||||
newUser(user).then(location => {
|
newUser(user).then(location => {
|
||||||
this.$router.push({ path: location })
|
this.$router.push({ path: location })
|
||||||
this.$store.commit('showSuccess', this.$t('settings.userCreated'))
|
this.$showSuccess(this.$t('settings.userCreated'))
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
this.$store.commit('showError', e)
|
this.$showError(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -228,9 +236,9 @@ export default {
|
|||||||
this.$store.commit('setUser', user)
|
this.$store.commit('setUser', user)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit('showSuccess', this.$t('settings.userUpdated'))
|
this.$showSuccess(this.$t('settings.userUpdated'))
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
this.$store.commit('showError', e)
|
this.$showError(e)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
parseForm () {
|
parseForm () {
|
||||||
@@ -238,6 +246,7 @@ export default {
|
|||||||
ID: this.id,
|
ID: this.id,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
|
lockPassword: this.lockPassword,
|
||||||
filesystem: this.filesystem,
|
filesystem: this.filesystem,
|
||||||
admin: this.admin,
|
admin: this.admin,
|
||||||
allowCommands: this.allowCommands,
|
allowCommands: this.allowCommands,
|
||||||
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>
|
||||||
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,17 +4,11 @@
|
|||||||
package filemanager
|
package filemanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -28,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,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
|
||||||
}
|
}
|
||||||
@@ -64,107 +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 := ""
|
|
||||||
noAuth := false
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
case "no_auth":
|
|
||||||
if !c.NextArg() {
|
|
||||||
noAuth = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
noAuth, err = strconv.ParseBool(c.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
caddyConf := httpserver.GetConfig(c)
|
|
||||||
|
|
||||||
path := filepath.Join(caddy.AssetsPath(), "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{
|
|
||||||
Locale: "en",
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
fm.NoAuth = noAuth
|
|
||||||
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,169 +1,15 @@
|
|||||||
package hugo
|
package hugo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 := ""
|
|
||||||
noAuth := false
|
|
||||||
|
|
||||||
// Get the baseURL and baseScope
|
|
||||||
args := c.RemainingArgs()
|
|
||||||
|
|
||||||
if len(args) >= 1 {
|
|
||||||
directory = args[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) > 1 {
|
|
||||||
admin = args[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for c.NextBlock() {
|
|
||||||
switch c.Val() {
|
|
||||||
case "database":
|
|
||||||
if !c.NextArg() {
|
|
||||||
return nil, c.ArgErr()
|
|
||||||
}
|
|
||||||
|
|
||||||
database = c.Val()
|
|
||||||
case "no_auth":
|
|
||||||
if !c.NextArg() {
|
|
||||||
noAuth = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
noAuth, err = strconv.ParseBool(c.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
caddyConf := httpserver.GetConfig(c)
|
|
||||||
|
|
||||||
path := filepath.Join(caddy.AssetsPath(), "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{
|
|
||||||
Locale: "en",
|
|
||||||
AllowCommands: true,
|
|
||||||
AllowEdit: true,
|
|
||||||
AllowNew: true,
|
|
||||||
AllowPublish: true,
|
|
||||||
Commands: []string{"git", "svn", "hg"},
|
|
||||||
Rules: []*filemanager.Rule{{
|
|
||||||
Regex: true,
|
|
||||||
Allow: false,
|
|
||||||
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
|
||||||
}},
|
|
||||||
CSS: "",
|
|
||||||
FileSystem: fileutils.Dir(directory),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the default settings for Hugo.
|
|
||||||
hugo := &filemanager.Hugo{
|
|
||||||
Root: directory,
|
|
||||||
Public: filepath.Join(directory, "public"),
|
|
||||||
Args: []string{},
|
|
||||||
CleanPublic: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attaches Hugo plugin to this file manager instance.
|
|
||||||
err = m.EnableStaticGen(hugo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.NoAuth = noAuth
|
|
||||||
m.SetBaseURL(admin)
|
|
||||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
|
||||||
configs = append(configs, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
||||||
@@ -175,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,169 +1,15 @@
|
|||||||
package jekyll
|
package jekyll
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setup configures a new FileManager middleware instance.
|
|
||||||
func setup(c *caddy.Controller) error {
|
|
||||||
configs, err := parse(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
|
||||||
return plugin{Configs: configs, Next: next}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
|
||||||
var (
|
|
||||||
configs []*filemanager.FileManager
|
|
||||||
)
|
|
||||||
|
|
||||||
for c.Next() {
|
|
||||||
// jekyll [directory] [admin] {
|
|
||||||
// database path
|
|
||||||
// }
|
|
||||||
directory := "."
|
|
||||||
admin := "/admin"
|
|
||||||
database := ""
|
|
||||||
noAuth := false
|
|
||||||
|
|
||||||
// Get the baseURL and baseScope
|
|
||||||
args := c.RemainingArgs()
|
|
||||||
|
|
||||||
if len(args) >= 1 {
|
|
||||||
directory = args[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) > 1 {
|
|
||||||
admin = args[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for c.NextBlock() {
|
|
||||||
switch c.Val() {
|
|
||||||
case "database":
|
|
||||||
if !c.NextArg() {
|
|
||||||
return nil, c.ArgErr()
|
|
||||||
}
|
|
||||||
|
|
||||||
database = c.Val()
|
|
||||||
case "no_auth":
|
|
||||||
if !c.NextArg() {
|
|
||||||
noAuth = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
noAuth, err = strconv.ParseBool(c.Val())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
caddyConf := httpserver.GetConfig(c)
|
|
||||||
|
|
||||||
path := filepath.Join(caddy.AssetsPath(), "jekyll")
|
|
||||||
err := os.MkdirAll(path, 0700)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a database path and it is not absolute,
|
|
||||||
// it will be relative to ".caddy" folder.
|
|
||||||
if !filepath.IsAbs(database) && database != "" {
|
|
||||||
database = filepath.Join(path, database)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no database path on the settings,
|
|
||||||
// store one in .caddy/jekyll/{name}.db.
|
|
||||||
if database == "" {
|
|
||||||
// The name of the database is the hashed value of a string composed
|
|
||||||
// by the host, address path and the baseurl of this File Manager
|
|
||||||
// instance.
|
|
||||||
hasher := md5.New()
|
|
||||||
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + admin))
|
|
||||||
sha := hex.EncodeToString(hasher.Sum(nil))
|
|
||||||
database = filepath.Join(path, sha+".db")
|
|
||||||
|
|
||||||
fmt.Println("[WARNING] A database is going to be created for your Jekyll instace at " + database +
|
|
||||||
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := filemanager.New(database, filemanager.User{
|
|
||||||
Locale: "en",
|
|
||||||
AllowCommands: true,
|
|
||||||
AllowEdit: true,
|
|
||||||
AllowNew: true,
|
|
||||||
AllowPublish: true,
|
|
||||||
Commands: []string{"git", "svn", "hg"},
|
|
||||||
Rules: []*filemanager.Rule{{
|
|
||||||
Regex: true,
|
|
||||||
Allow: false,
|
|
||||||
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
|
||||||
}},
|
|
||||||
CSS: "",
|
|
||||||
FileSystem: fileutils.Dir(directory),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the default settings for Jekyll.
|
|
||||||
jekyll := &filemanager.Jekyll{
|
|
||||||
Root: directory,
|
|
||||||
Public: filepath.Join(directory, "_site"),
|
|
||||||
Args: []string{},
|
|
||||||
CleanPublic: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attaches Hugo plugin to this file manager instance.
|
|
||||||
err = m.EnableStaticGen(jekyll)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.NoAuth = noAuth
|
|
||||||
m.SetBaseURL(admin)
|
|
||||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
|
||||||
configs = append(configs, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
return configs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
|
||||||
func (p plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
for i := range p.Configs {
|
|
||||||
// Checks if this Path should be handled by File Manager.
|
|
||||||
if !httpserver.Path(r.URL.Path).Matches(p.Configs[i].BaseURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Configs[i].ServeHTTP(w, r)
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.Next.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterPlugin("jekyll", caddy.Plugin{
|
caddy.RegisterPlugin("jekyll", caddy.Plugin{
|
||||||
ServerType: "http",
|
ServerType: "http",
|
||||||
@@ -175,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, "jekyll")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||||
|
return plugin{Configs: configs, Next: next}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
294
caddy/parser/parser.go
Normal file
294
caddy/parser/parser.go
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/asdine/storm"
|
||||||
|
"github.com/hacdias/filemanager"
|
||||||
|
"github.com/hacdias/filemanager/bolt"
|
||||||
|
"github.com/hacdias/filemanager/staticgen"
|
||||||
|
"github.com/hacdias/fileutils"
|
||||||
|
"github.com/mholt/caddy"
|
||||||
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
var databases = map[string]*storm.DB{}
|
||||||
|
|
||||||
|
// Parse ...
|
||||||
|
func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) {
|
||||||
|
var (
|
||||||
|
configs []*filemanager.FileManager
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
for c.Next() {
|
||||||
|
u := &filemanager.User{
|
||||||
|
Locale: "en",
|
||||||
|
AllowCommands: true,
|
||||||
|
AllowEdit: true,
|
||||||
|
AllowNew: true,
|
||||||
|
AllowPublish: true,
|
||||||
|
Commands: []string{"git", "svn", "hg"},
|
||||||
|
CSS: "",
|
||||||
|
Rules: []*filemanager.Rule{{
|
||||||
|
Regex: true,
|
||||||
|
Allow: false,
|
||||||
|
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := "/"
|
||||||
|
scope := "."
|
||||||
|
database := ""
|
||||||
|
noAuth := false
|
||||||
|
reCaptchaKey := ""
|
||||||
|
reCaptchaSecret := ""
|
||||||
|
|
||||||
|
if plugin != "" {
|
||||||
|
baseURL = "/admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the baseURL and scope
|
||||||
|
args := c.RemainingArgs()
|
||||||
|
|
||||||
|
if plugin == "" {
|
||||||
|
if len(args) >= 1 {
|
||||||
|
baseURL = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
scope = args[1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(args) >= 1 {
|
||||||
|
scope = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 1 {
|
||||||
|
baseURL = args[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for c.NextBlock() {
|
||||||
|
switch c.Val() {
|
||||||
|
case "database":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
database = c.Val()
|
||||||
|
case "locale":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Locale = c.Val()
|
||||||
|
case "allow_commands":
|
||||||
|
if !c.NextArg() {
|
||||||
|
u.AllowCommands = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.AllowCommands, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "allow_edit":
|
||||||
|
if !c.NextArg() {
|
||||||
|
u.AllowEdit = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.AllowEdit, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "allow_new":
|
||||||
|
if !c.NextArg() {
|
||||||
|
u.AllowNew = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.AllowNew, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "allow_publish":
|
||||||
|
if !c.NextArg() {
|
||||||
|
u.AllowPublish = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.AllowPublish, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "commands":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Commands = strings.Split(c.Val(), " ")
|
||||||
|
case "css":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
file := c.Val()
|
||||||
|
css, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.CSS = string(css)
|
||||||
|
case "view_mode":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.ViewMode = c.Val()
|
||||||
|
if u.ViewMode != "mosaic" && u.ViewMode != "list" {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
case "recaptcha_key":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
reCaptchaKey = c.Val()
|
||||||
|
case "recaptcha_secret":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
reCaptchaSecret = c.Val()
|
||||||
|
case "no_auth":
|
||||||
|
if !c.NextArg() {
|
||||||
|
noAuth = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
noAuth, err = strconv.ParseBool(c.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
caddyConf := httpserver.GetConfig(c)
|
||||||
|
|
||||||
|
path := filepath.Join(caddy.AssetsPath(), "filemanager")
|
||||||
|
err := os.MkdirAll(path, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a database path and it is not absolute,
|
||||||
|
// it will be relative to Caddy folder.
|
||||||
|
if !filepath.IsAbs(database) && database != "" {
|
||||||
|
database = filepath.Join(path, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no database path on the settings,
|
||||||
|
// store one in .caddy/filemanager/name.db.
|
||||||
|
if database == "" {
|
||||||
|
// The name of the database is the hashed value of a string composed
|
||||||
|
// by the host, address path and the baseurl of this File Manager
|
||||||
|
// instance.
|
||||||
|
hasher := md5.New()
|
||||||
|
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL))
|
||||||
|
sha := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
database = filepath.Join(path, sha+".db")
|
||||||
|
|
||||||
|
fmt.Println("[WARNING] A database is going to be created for your File Manager instace at " + database +
|
||||||
|
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Scope = scope
|
||||||
|
u.FileSystem = fileutils.Dir(scope)
|
||||||
|
|
||||||
|
var db *storm.DB
|
||||||
|
if stored, ok := databases[database]; ok {
|
||||||
|
db = stored
|
||||||
|
} else {
|
||||||
|
db, err = storm.Open(database)
|
||||||
|
databases[database] = db
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &filemanager.FileManager{
|
||||||
|
NoAuth: noAuth,
|
||||||
|
BaseURL: "",
|
||||||
|
PrefixURL: "",
|
||||||
|
ReCaptchaKey: reCaptchaKey,
|
||||||
|
ReCaptchaSecret: reCaptchaSecret,
|
||||||
|
DefaultUser: u,
|
||||||
|
Store: &filemanager.Store{
|
||||||
|
Config: bolt.ConfigStore{DB: db},
|
||||||
|
Users: bolt.UsersStore{DB: db},
|
||||||
|
Share: bolt.ShareStore{DB: db},
|
||||||
|
},
|
||||||
|
NewFS: func(scope string) filemanager.FileSystem {
|
||||||
|
return fileutils.Dir(scope)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.Setup()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch plugin {
|
||||||
|
case "hugo":
|
||||||
|
// Initialize the default settings for Hugo.
|
||||||
|
hugo := &staticgen.Hugo{
|
||||||
|
Root: scope,
|
||||||
|
Public: filepath.Join(scope, "public"),
|
||||||
|
Args: []string{},
|
||||||
|
CleanPublic: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches Hugo plugin to this file manager instance.
|
||||||
|
err = m.Attach(hugo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "jekyll":
|
||||||
|
// Initialize the default settings for Jekyll.
|
||||||
|
jekyll := &staticgen.Jekyll{
|
||||||
|
Root: scope,
|
||||||
|
Public: filepath.Join(scope, "_site"),
|
||||||
|
Args: []string{},
|
||||||
|
CleanPublic: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches Hugo plugin to this file manager instance.
|
||||||
|
err = m.Attach(jekyll)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.NoAuth = noAuth
|
||||||
|
m.SetBaseURL(baseURL)
|
||||||
|
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
||||||
|
|
||||||
|
configs = append(configs, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
@@ -10,31 +10,40 @@ 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"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
addr string
|
addr string
|
||||||
config string
|
config string
|
||||||
database string
|
database string
|
||||||
scope string
|
scope string
|
||||||
commands string
|
commands string
|
||||||
logfile string
|
logfile string
|
||||||
staticgen string
|
staticg string
|
||||||
locale string
|
locale string
|
||||||
port int
|
baseurl string
|
||||||
noAuth bool
|
prefixurl string
|
||||||
allowCommands bool
|
viewMode string
|
||||||
allowEdit bool
|
recaptchakey string
|
||||||
allowNew bool
|
recaptchasecret string
|
||||||
allowPublish bool
|
port int
|
||||||
showVer bool
|
noAuth bool
|
||||||
version = "master"
|
allowCommands bool
|
||||||
|
allowEdit bool
|
||||||
|
allowNew bool
|
||||||
|
allowPublish bool
|
||||||
|
showVer bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -44,14 +53,19 @@ 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.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key")
|
||||||
|
flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret")
|
||||||
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users")
|
flag.BoolVar(&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(&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.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
|
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
|
||||||
flag.StringVar(&locale, "locale", "en", "Default locale for new users")
|
flag.StringVar(&locale, "locale", "en", "Default locale for new users")
|
||||||
flag.StringVar(&staticgen, "staticgen", "", "Static Generator you want to enable")
|
flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable")
|
||||||
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
|
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +83,11 @@ func setupViper() {
|
|||||||
viper.SetDefault("StaticGen", "")
|
viper.SetDefault("StaticGen", "")
|
||||||
viper.SetDefault("Locale", "en")
|
viper.SetDefault("Locale", "en")
|
||||||
viper.SetDefault("NoAuth", false)
|
viper.SetDefault("NoAuth", false)
|
||||||
|
viper.SetDefault("BaseURL", "")
|
||||||
|
viper.SetDefault("PrefixURL", "")
|
||||||
|
viper.SetDefault("ViewMode", "mosaic")
|
||||||
|
viper.SetDefault("ReCaptchaKey", "")
|
||||||
|
viper.SetDefault("ReCaptchaSecret", "")
|
||||||
|
|
||||||
viper.BindPFlag("Port", flag.Lookup("port"))
|
viper.BindPFlag("Port", flag.Lookup("port"))
|
||||||
viper.BindPFlag("Address", flag.Lookup("address"))
|
viper.BindPFlag("Address", flag.Lookup("address"))
|
||||||
@@ -83,21 +102,18 @@ func setupViper() {
|
|||||||
viper.BindPFlag("Locale", flag.Lookup("locale"))
|
viper.BindPFlag("Locale", flag.Lookup("locale"))
|
||||||
viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
|
viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
|
||||||
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
|
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
|
||||||
|
viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
|
||||||
|
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
|
||||||
|
viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
|
||||||
|
viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key"))
|
||||||
|
viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret"))
|
||||||
|
|
||||||
viper.SetConfigName("filemanager")
|
viper.SetConfigName("filemanager")
|
||||||
viper.AddConfigPath(".")
|
viper.AddConfigPath(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
func printVersion() {
|
func printVersion() {
|
||||||
version = strings.TrimSpace(version)
|
fmt.Println("filemanager version", filemanager.Version)
|
||||||
|
|
||||||
if version == "" {
|
|
||||||
fmt.Println("filemanager is at an untracked version")
|
|
||||||
} else {
|
|
||||||
version = strings.TrimPrefix(version, "v")
|
|
||||||
fmt.Println("filemanager version", version)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,52 +164,6 @@ func main() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a File Manager instance.
|
|
||||||
fm, err := filemanager.New(viper.GetString("Database"), filemanager.User{
|
|
||||||
AllowCommands: viper.GetBool("AllowCommands"),
|
|
||||||
AllowEdit: viper.GetBool("AllowEdit"),
|
|
||||||
AllowNew: viper.GetBool("AllowNew"),
|
|
||||||
AllowPublish: viper.GetBool("AllowPublish"),
|
|
||||||
Commands: viper.GetStringSlice("Commands"),
|
|
||||||
Rules: []*filemanager.Rule{},
|
|
||||||
Locale: viper.GetString("Locale"),
|
|
||||||
CSS: "",
|
|
||||||
FileSystem: fileutils.Dir(viper.GetString("Scope")),
|
|
||||||
})
|
|
||||||
|
|
||||||
if viper.GetBool("NoAuth") {
|
|
||||||
fm.NoAuth = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch viper.GetString("StaticGen") {
|
|
||||||
case "hugo":
|
|
||||||
hugo := &filemanager.Hugo{
|
|
||||||
Root: viper.GetString("Scope"),
|
|
||||||
Public: filepath.Join(viper.GetString("Scope"), "public"),
|
|
||||||
Args: []string{},
|
|
||||||
CleanPublic: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = fm.EnableStaticGen(hugo); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "jekyll":
|
|
||||||
jekyll := &filemanager.Jekyll{
|
|
||||||
Root: viper.GetString("Scope"),
|
|
||||||
Public: filepath.Join(viper.GetString("Scope"), "_site"),
|
|
||||||
Args: []string{"build"},
|
|
||||||
CleanPublic: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = fm.EnableStaticGen(jekyll); 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)
|
||||||
@@ -205,7 +175,75 @@ 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"),
|
||||||
|
ReCaptchaKey: viper.GetString("ReCaptchaKey"),
|
||||||
|
ReCaptchaSecret: viper.GetString("ReCaptchaSecret"),
|
||||||
|
DefaultUser: &filemanager.User{
|
||||||
|
AllowCommands: viper.GetBool("AllowCommands"),
|
||||||
|
AllowEdit: viper.GetBool("AllowEdit"),
|
||||||
|
AllowNew: viper.GetBool("AllowNew"),
|
||||||
|
AllowPublish: viper.GetBool("AllowPublish"),
|
||||||
|
Commands: viper.GetStringSlice("Commands"),
|
||||||
|
Rules: []*filemanager.Rule{},
|
||||||
|
Locale: viper.GetString("Locale"),
|
||||||
|
CSS: "",
|
||||||
|
Scope: viper.GetString("Scope"),
|
||||||
|
FileSystem: fileutils.Dir(viper.GetString("Scope")),
|
||||||
|
ViewMode: viper.GetString("ViewMode"),
|
||||||
|
},
|
||||||
|
Store: &filemanager.Store{
|
||||||
|
Config: bolt.ConfigStore{DB: db},
|
||||||
|
Users: bolt.UsersStore{DB: db},
|
||||||
|
Share: bolt.ShareStore{DB: db},
|
||||||
|
},
|
||||||
|
NewFS: func(scope string) filemanager.FileSystem {
|
||||||
|
return fileutils.Dir(scope)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fm.Setup()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch viper.GetString("StaticGen") {
|
||||||
|
case "hugo":
|
||||||
|
hugo := &staticgen.Hugo{
|
||||||
|
Root: viper.GetString("Scope"),
|
||||||
|
Public: filepath.Join(viper.GetString("Scope"), "public"),
|
||||||
|
Args: []string{},
|
||||||
|
CleanPublic: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = fm.Attach(hugo); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
case "jekyll":
|
||||||
|
jekyll := &staticgen.Jekyll{
|
||||||
|
Root: viper.GetString("Scope"),
|
||||||
|
Public: filepath.Join(viper.GetString("Scope"), "_site"),
|
||||||
|
Args: []string{"build"},
|
||||||
|
CleanPublic: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = fm.Attach(jekyll); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Handler(fm)
|
||||||
|
}
|
||||||
|
|||||||
73
doc.go
Normal file
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
|
||||||
72
file.go
72
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"
|
||||||
@@ -23,13 +22,9 @@ import (
|
|||||||
"github.com/gohugoio/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.File.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 {
|
||||||
|
|||||||
668
filemanager.go
668
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"
|
||||||
@@ -64,38 +13,44 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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"
|
"github.com/robfig/cron"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Version is the current File Manager version.
|
||||||
|
const Version = "1.3.5"
|
||||||
|
|
||||||
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")
|
||||||
errEmptyUsername = errors.New("username is empty")
|
ErrEmptyUsername = errors.New("username is empty")
|
||||||
errEmptyScope = errors.New("scope is empty")
|
ErrEmptyScope = errors.New("scope is empty")
|
||||||
errWrongDataType = errors.New("wrong data type")
|
ErrWrongDataType = errors.New("wrong data type")
|
||||||
errInvalidUpdateField = errors.New("invalid field to update")
|
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
|
||||||
|
|
||||||
// Job cron.
|
// The Store is used to manage users, shareable links and
|
||||||
cron *cron.Cron
|
// 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
|
||||||
@@ -111,174 +66,130 @@ type FileManager struct {
|
|||||||
// there will only exist one user, called "admin".
|
// there will only exist one user, called "admin".
|
||||||
NoAuth bool
|
NoAuth bool
|
||||||
|
|
||||||
// staticgen is the name of the current static website generator.
|
// ReCaptcha Site key and secret.
|
||||||
staticgen string
|
ReCaptchaKey string
|
||||||
|
ReCaptchaSecret string
|
||||||
|
|
||||||
// StaticGen is the static websit generator handler.
|
// StaticGen is the static websit generator handler.
|
||||||
StaticGen StaticGen
|
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
|
||||||
|
|
||||||
|
// Global stylesheet.
|
||||||
|
CSS string
|
||||||
|
|
||||||
|
// NewFS should build a new file system for a given path.
|
||||||
|
NewFS FSBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandEvents = []string{
|
||||||
|
"before_save",
|
||||||
|
"after_save",
|
||||||
|
"before_publish",
|
||||||
|
"after_publish",
|
||||||
|
"before_copy",
|
||||||
|
"after_copy",
|
||||||
|
"before_rename",
|
||||||
|
"after_rename",
|
||||||
|
"before_upload",
|
||||||
|
"after_upload",
|
||||||
|
"before_delete",
|
||||||
|
"after_delete",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command is a command function.
|
// 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"`
|
|
||||||
|
|
||||||
// Locale is the language of the user.
|
|
||||||
Locale string `json:"locale"`
|
|
||||||
|
|
||||||
// These indicate if the user can perform certain actions.
|
|
||||||
AllowNew bool `json:"allowNew"` // Create files and folders
|
|
||||||
AllowEdit bool `json:"allowEdit"` // Edit/rename files
|
|
||||||
AllowCommands bool `json:"allowCommands"` // Execute commands
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultUser is used on New, when no 'base' user is provided.
|
|
||||||
var DefaultUser = User{
|
|
||||||
AllowCommands: true,
|
|
||||||
AllowEdit: true,
|
|
||||||
AllowNew: true,
|
|
||||||
AllowPublish: true,
|
|
||||||
Commands: []string{},
|
|
||||||
Rules: []*Rule{},
|
|
||||||
CSS: "",
|
|
||||||
Admin: true,
|
|
||||||
Locale: "en",
|
|
||||||
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()
|
||||||
cron: cron.New(),
|
|
||||||
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 {
|
||||||
"before_publish": {},
|
if _, ok := m.Commands[command]; ok {
|
||||||
"after_publish": {},
|
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.
|
||||||
@@ -289,25 +200,26 @@ func New(database string, base User) (*FileManager, error) {
|
|||||||
u.AllowPublish = 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.
|
// TODO: remove this after 1.5
|
||||||
m.db = db
|
for _, user := range users {
|
||||||
|
if user.ViewMode != "list" && user.ViewMode != "mosaic" {
|
||||||
|
user.ViewMode = "list"
|
||||||
|
m.Store.Users.Update(user, "ViewMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the default user, making a copy of the base.
|
m.DefaultUser.Username = ""
|
||||||
base.Username = ""
|
m.DefaultUser.Password = ""
|
||||||
base.Password = ""
|
|
||||||
m.DefaultUser = &base
|
|
||||||
|
|
||||||
m.cron.AddFunc("@hourly", m.shareCleaner)
|
m.Cron.AddFunc("@hourly", m.ShareCleaner)
|
||||||
m.cron.Start()
|
m.Cron.Start()
|
||||||
|
|
||||||
return m, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RootURL returns the actual URL where
|
// RootURL returns the actual URL where
|
||||||
@@ -334,95 +246,32 @@ func (m *FileManager) SetBaseURL(url string) {
|
|||||||
m.BaseURL = strings.TrimSuffix(url, "/")
|
m.BaseURL = strings.TrimSuffix(url, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles the request.
|
// Attach attaches a static generator to the current File Manager.
|
||||||
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (m *FileManager) Attach(s StaticGen) error {
|
||||||
code, err := serveHTTP(&RequestContext{
|
if reflect.TypeOf(s).Kind() != reflect.Ptr {
|
||||||
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()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnableStaticGen attaches a static generator to the current File Manager
|
|
||||||
// instance.
|
|
||||||
func (m *FileManager) EnableStaticGen(data StaticGen) error {
|
|
||||||
if reflect.TypeOf(data).Kind() != reflect.Ptr {
|
|
||||||
return errors.New("data should be a pointer to interface, not interface")
|
return errors.New("data should be a pointer to interface, not interface")
|
||||||
}
|
}
|
||||||
|
|
||||||
if h, ok := data.(*Hugo); ok {
|
err := s.Setup()
|
||||||
return m.enableHugo(h)
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if j, ok := data.(*Jekyll); ok {
|
|
||||||
return m.enableJekyll(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("unknown static website generator")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FileManager) enableHugo(h *Hugo) error {
|
|
||||||
if err := h.find(); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.staticgen = "hugo"
|
m.StaticGen = s
|
||||||
m.StaticGen = h
|
|
||||||
|
|
||||||
err := m.db.Get("staticgen", "hugo", h)
|
err = m.Store.Config.Get("staticgen_"+s.Name(), s)
|
||||||
if err != nil && err == storm.ErrNotFound {
|
if err == ErrNotExist {
|
||||||
err = m.db.Set("staticgen", "hugo", *h)
|
return m.Store.Config.Save("staticgen_"+s.Name(), s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FileManager) enableJekyll(j *Jekyll) error {
|
// ShareCleaner removes sharing links that are no longer active.
|
||||||
if err := j.find(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(j.Args) == 0 {
|
|
||||||
j.Args = []string{"build"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if j.Args[0] != "build" {
|
|
||||||
j.Args = append([]string{"build"}, j.Args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.staticgen = "jekyll"
|
|
||||||
m.StaticGen = j
|
|
||||||
|
|
||||||
err := m.db.Get("staticgen", "jekyll", j)
|
|
||||||
if err != nil && err == storm.ErrNotFound {
|
|
||||||
err = m.db.Set("staticgen", "jekyll", *j)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// shareCleaner removes sharing links that are no longer active.
|
|
||||||
// This function is set to run periodically.
|
// This function is set to run periodically.
|
||||||
func (m FileManager) shareCleaner() {
|
func (m FileManager) ShareCleaner() {
|
||||||
var links []shareLink
|
|
||||||
|
|
||||||
// Get all links.
|
// Get all links.
|
||||||
err := m.db.All(&links)
|
links, err := m.Store.Share.Gets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
return
|
return
|
||||||
@@ -431,7 +280,7 @@ func (m FileManager) shareCleaner() {
|
|||||||
// Find the expired ones.
|
// Find the expired ones.
|
||||||
for i := range links {
|
for i := range links {
|
||||||
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
|
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
|
||||||
err = m.db.DeleteStruct(&links[i])
|
err = m.Store.Share.Delete(links[i].Hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
}
|
}
|
||||||
@@ -439,39 +288,8 @@ func (m FileManager) shareCleaner() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -496,7 +314,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
|
||||||
@@ -518,3 +344,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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,67 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type cred struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Recaptcha string `json:"recaptcha"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// recaptcha checks the recaptcha code.
|
||||||
|
func recaptcha(secret string, response string) (bool, error) {
|
||||||
|
api := "https://www.google.com/recaptcha/api/siteverify"
|
||||||
|
|
||||||
|
body := url.Values{}
|
||||||
|
body.Set("secret", secret)
|
||||||
|
body.Add("response", response)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Post(api, "application/x-www-form-urlencoded", bytes.NewBufferString(body.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ChallengeTS time.Time `json:"challenge_ts"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
ErrorCodes interface{} `json:"error-codes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Success, nil
|
||||||
|
}
|
||||||
|
|
||||||
// authHandler proccesses the authentication for the user.
|
// 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.
|
// NoAuth instances shouldn't call this method.
|
||||||
if c.NoAuth {
|
if c.NoAuth {
|
||||||
return 0, nil
|
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 cred
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
@@ -31,14 +71,27 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
|
|||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If ReCaptcha is enabled, check the code.
|
||||||
|
if len(c.ReCaptchaSecret) > 0 {
|
||||||
|
ok, err := recaptcha(c.ReCaptchaSecret, cred.Recaptcha)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return http.StatusForbidden, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Checks if the user exists.
|
// Checks if the user exists.
|
||||||
u, ok := c.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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +101,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
|
|||||||
|
|
||||||
// renewAuthHandler is used when the front-end already has a JWT token
|
// 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
|
||||||
@@ -60,23 +113,21 @@ 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
|
||||||
NoAuth bool `json:"noAuth"`
|
|
||||||
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 = ""
|
||||||
|
|
||||||
// Builds the claims.
|
// Builds the claims.
|
||||||
claims := claims{
|
claims := claims{
|
||||||
u,
|
u,
|
||||||
c.NoAuth,
|
|
||||||
jwt.StandardClaims{
|
jwt.StandardClaims{
|
||||||
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
|
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
|
||||||
Issuer: "File Manager",
|
Issuer: "File Manager",
|
||||||
@@ -85,7 +136,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)
|
||||||
signed, err := token.SignedString(c.key)
|
signed, err := token.SignedString(c.Key)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
@@ -119,14 +170,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 {
|
if c.NoAuth {
|
||||||
c.User = c.DefaultUser
|
c.User = c.DefaultUser
|
||||||
return true, c.User
|
return true, c.User
|
||||||
}
|
}
|
||||||
|
|
||||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||||
return c.key, nil
|
return c.Key, nil
|
||||||
}
|
}
|
||||||
var claims claims
|
var claims claims
|
||||||
token, err := request.ParseFromRequestWithClaims(r,
|
token, err := request.ParseFromRequestWithClaims(r,
|
||||||
@@ -139,38 +190,11 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
u, ok := c.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,13 +9,14 @@ 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
|
||||||
@@ -24,7 +25,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
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.File.Name)
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+c.File.Name+"\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
http.ServeFile(w, r, c.File.Path)
|
http.ServeFile(w, r, c.File.Path)
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,48 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asdine/storm"
|
fm "github.com/hacdias/filemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequestContext contains the needed information to make handlers work.
|
// Handler returns a function compatible with http.HandleFunc.
|
||||||
type RequestContext struct {
|
func Handler(m *fm.FileManager) http.Handler {
|
||||||
*FileManager
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
User *User
|
code, err := serve(&fm.Context{
|
||||||
File *file
|
FileManager: m,
|
||||||
// On API handlers, Router is the APi handler we want.
|
User: nil,
|
||||||
Router string
|
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()))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveHTTP is the main entry point of this HTML application.
|
// serve is the main entry point of this HTML application.
|
||||||
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
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
|
// 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!
|
// returns a 404 fm.Error because we're not supposed to be here!
|
||||||
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
|
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
|
||||||
|
|
||||||
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
|
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
|
||||||
@@ -35,11 +54,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
|||||||
// Check if this request is made to the service worker. If so,
|
// Check if this request is made to the service worker. If so,
|
||||||
// pass it through a template to add the needed variables.
|
// pass it through a template to add the needed variables.
|
||||||
if r.URL.Path == "/sw.js" {
|
if r.URL.Path == "/sw.js" {
|
||||||
return renderFile(
|
return renderFile(c, w, "sw.js")
|
||||||
c, w,
|
|
||||||
c.assets.MustString("sw.js"),
|
|
||||||
"application/javascript",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if this request is made to the static assets folder. If so, and
|
// Checks if this request is made to the static assets folder. If so, and
|
||||||
@@ -67,7 +82,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
|||||||
return c.StaticGen.Preview(c, w, r)
|
return c.StaticGen.Preview(c, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/share/") && c.StaticGen != nil {
|
if strings.HasPrefix(r.URL.Path, "/share/") {
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
|
||||||
return sharePage(c, w, r)
|
return sharePage(c, w, r)
|
||||||
}
|
}
|
||||||
@@ -77,29 +92,21 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
|||||||
w.Header().Set("x-content-type", "nosniff")
|
w.Header().Set("x-content-type", "nosniff")
|
||||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||||
|
|
||||||
return renderFile(
|
return renderFile(c, w, "index.html")
|
||||||
c, w,
|
|
||||||
c.assets.MustString("index.html"),
|
|
||||||
"text/html",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// staticHandler handles the static assets path.
|
// staticHandler handles the static assets path.
|
||||||
func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if r.URL.Path != "/static/manifest.json" {
|
if r.URL.Path != "/static/manifest.json" {
|
||||||
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
|
http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderFile(
|
return renderFile(c, w, "static/manifest.json")
|
||||||
c, w,
|
|
||||||
c.assets.MustString("static/manifest.json"),
|
|
||||||
"application/json",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiHandler is the main entry point for the /api endpoint.
|
// apiHandler is the main entry point for the /api endpoint.
|
||||||
func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if r.URL.Path == "/auth/get" {
|
if r.URL.Path == "/auth/get" {
|
||||||
return authHandler(c, w, r)
|
return authHandler(c, w, r)
|
||||||
}
|
}
|
||||||
@@ -135,9 +142,9 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
|||||||
|
|
||||||
if c.Router == "checksum" || c.Router == "download" {
|
if c.Router == "checksum" || c.Router == "download" {
|
||||||
var err error
|
var err error
|
||||||
c.File, err = getInfo(r.URL, c.FileManager, c.User)
|
c.File, 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,11 +176,11 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
|
// 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) {
|
func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
query := r.URL.Query().Get("algo")
|
query := r.URL.Query().Get("algo")
|
||||||
|
|
||||||
val, err := c.File.Checksum(query)
|
val, err := c.File.Checksum(query)
|
||||||
if err == errInvalidOption {
|
if err == fm.ErrInvalidOption {
|
||||||
return http.StatusBadRequest, err
|
return http.StatusBadRequest, err
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
@@ -201,14 +208,39 @@ func splitURL(path string) (string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// renderFile renders a file using a template with some needed variables.
|
// renderFile renders a file using a template with some needed variables.
|
||||||
func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentType string) (int, error) {
|
func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) {
|
||||||
tpl := template.Must(template.New("file").Parse(file))
|
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")
|
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
||||||
|
|
||||||
err := tpl.Execute(w, map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"BaseURL": c.RootURL(),
|
"BaseURL": c.RootURL(),
|
||||||
"StaticGen": c.staticgen,
|
"NoAuth": c.NoAuth,
|
||||||
})
|
"Version": fm.Version,
|
||||||
|
"CSS": template.CSS(c.CSS),
|
||||||
|
"ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "",
|
||||||
|
"ReCaptchaKey": c.ReCaptchaKey,
|
||||||
|
"ReCaptchaSecret": c.ReCaptchaSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.StaticGen != nil {
|
||||||
|
data["StaticGen"] = c.StaticGen.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tpl.Execute(w, data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -216,15 +248,12 @@ func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentTy
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
// sharePage build the share page.
|
||||||
var s shareLink
|
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
err := c.db.One("Hash", r.URL.Path, &s)
|
s, err := c.Store.Share.Get(r.URL.Path)
|
||||||
if err == storm.ErrNotFound {
|
if err == fm.ErrNotExist {
|
||||||
return renderFile(
|
w.WriteHeader(http.StatusNotFound)
|
||||||
c, w,
|
return renderFile(c, w, "static/share/404.html")
|
||||||
c.assets.MustString("static/share/404.html"),
|
|
||||||
"text/html",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -232,22 +261,19 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.Expires && s.ExpireDate.Before(time.Now()) {
|
if s.Expires && s.ExpireDate.Before(time.Now()) {
|
||||||
c.db.DeleteStruct(&s)
|
c.Store.Share.Delete(s.Hash)
|
||||||
return renderFile(
|
w.WriteHeader(http.StatusNotFound)
|
||||||
c, w,
|
return renderFile(c, w, "static/share/404.html")
|
||||||
c.assets.MustString("static/share/404.html"),
|
|
||||||
"text/html",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.URL.Path = s.Path
|
r.URL.Path = s.Path
|
||||||
|
|
||||||
info, err := os.Stat(s.Path)
|
info, err := os.Stat(s.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorToHTTP(err, false), err
|
return ErrorToHTTP(err, false), err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.File = &file{
|
c.File = &fm.File{
|
||||||
Path: s.Path,
|
Path: s.Path,
|
||||||
Name: info.Name(),
|
Name: info.Name(),
|
||||||
ModTime: info.ModTime(),
|
ModTime: info.ModTime(),
|
||||||
@@ -259,7 +285,7 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
|||||||
dl := r.URL.Query().Get("dl")
|
dl := r.URL.Query().Get("dl")
|
||||||
|
|
||||||
if dl == "" || dl == "0" {
|
if dl == "" || dl == "0" {
|
||||||
tpl := template.Must(template.New("file").Parse(c.assets.MustString("static/share/index.html")))
|
tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html")))
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
err := tpl.Execute(w, map[string]interface{}{
|
err := tpl.Execute(w, map[string]interface{}{
|
||||||
@@ -299,8 +325,8 @@ func matchURL(first, second string) bool {
|
|||||||
return strings.HasPrefix(first, second)
|
return strings.HasPrefix(first, second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// errorToHTTP converts errors to HTTP Status Code.
|
// ErrorToHTTP converts errors to HTTP Status Code.
|
||||||
func errorToHTTP(err error, gone bool) int {
|
func ErrorToHTTP(err error, gone bool) int {
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return http.StatusOK
|
return http.StatusOK
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
"github.com/hacdias/fileutils"
|
"github.com/hacdias/fileutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,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 {
|
||||||
@@ -36,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.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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// After save command handler.
|
// After save command handler.
|
||||||
if err := c.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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,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.FileManager, 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,
|
||||||
@@ -82,7 +83,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
// 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
|
||||||
@@ -96,23 +97,23 @@ 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.File
|
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.RootURL()
|
cookieScope := c.RootURL()
|
||||||
@@ -129,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
|
||||||
}
|
}
|
||||||
@@ -174,7 +183,7 @@ 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, 0776)
|
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
|
||||||
@@ -186,23 +195,28 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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, 0776)
|
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
|
// Check if this instance has a Static Generator and handles publishing
|
||||||
@@ -217,10 +231,16 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
|
|||||||
// 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 *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
publish := r.Header.Get("Publish")
|
publish := r.Header.Get("Publish")
|
||||||
schedule := r.Header.Get("Schedule")
|
schedule := r.Header.Get("Schedule")
|
||||||
|
|
||||||
@@ -241,7 +261,7 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
|
|||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
|
c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
|
||||||
_, err := resourcePublish(c, w, r)
|
_, err := resourcePublish(c, w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@@ -251,11 +271,11 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
|
|||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||||
|
|
||||||
// Before save command handler.
|
// Before save command handler.
|
||||||
if err := c.Runner("before_publish", path); err != nil {
|
if err := c.Runner("before_publish", path, "", c.User); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +285,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Executed the before publish command.
|
// Executed the before publish command.
|
||||||
if err := c.Runner("before_publish", path); err != nil {
|
if err := c.Runner("before_publish", path, "", c.User); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +293,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -282,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
|
||||||
@@ -292,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',
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -6,12 +6,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
type modifySettingsRequest struct {
|
type modifySettingsRequest struct {
|
||||||
*modifyRequest
|
*modifyRequest
|
||||||
Data struct {
|
Data struct {
|
||||||
|
CSS string `json:"css"`
|
||||||
Commands map[string][]string `json:"commands"`
|
Commands map[string][]string `json:"commands"`
|
||||||
StaticGen map[string]interface{} `json:"staticGen"`
|
StaticGen map[string]interface{} `json:"staticGen"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
@@ -26,7 +28,7 @@ type option struct {
|
|||||||
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
|
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
|
||||||
// Checks if the request body is empty.
|
// Checks if the request body is empty.
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return nil, errEmptyRequest
|
return nil, fm.ErrEmptyRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses the request body and checks if it's well formed.
|
// Parses the request body and checks if it's well formed.
|
||||||
@@ -38,13 +40,13 @@ func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
|
|||||||
|
|
||||||
// Checks if the request type is right.
|
// Checks if the request type is right.
|
||||||
if mod.What != "settings" {
|
if mod.What != "settings" {
|
||||||
return nil, errWrongDataType
|
return nil, fm.ErrWrongDataType
|
||||||
}
|
}
|
||||||
|
|
||||||
return mod, nil
|
return mod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func settingsHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if r.URL.Path != "" && r.URL.Path != "/" {
|
if r.URL.Path != "" && r.URL.Path != "/" {
|
||||||
return http.StatusNotFound, nil
|
return http.StatusNotFound, nil
|
||||||
}
|
}
|
||||||
@@ -60,11 +62,12 @@ func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
type settingsGetRequest struct {
|
type settingsGetRequest struct {
|
||||||
|
CSS string `json:"css"`
|
||||||
Commands map[string][]string `json:"commands"`
|
Commands map[string][]string `json:"commands"`
|
||||||
StaticGen []option `json:"staticGen"`
|
StaticGen []option `json:"staticGen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func settingsGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if !c.User.Admin {
|
if !c.User.Admin {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
@@ -72,6 +75,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
result := &settingsGetRequest{
|
result := &settingsGetRequest{
|
||||||
Commands: c.Commands,
|
Commands: c.Commands,
|
||||||
StaticGen: []option{},
|
StaticGen: []option{},
|
||||||
|
CSS: c.CSS,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.StaticGen != nil {
|
if c.StaticGen != nil {
|
||||||
@@ -93,7 +97,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
return renderJSON(w, result)
|
return renderJSON(w, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func settingsPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if !c.User.Admin {
|
if !c.User.Admin {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
@@ -102,9 +106,10 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusBadRequest, err
|
return http.StatusBadRequest, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the commands.
|
// Update the commands.
|
||||||
if mod.Which == "commands" {
|
if mod.Which == "commands" {
|
||||||
if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil {
|
if err := c.Store.Config.Save("commands", mod.Data.Commands); err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +117,16 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the global CSS.
|
||||||
|
if mod.Which == "css" {
|
||||||
|
if err := c.Store.Config.Save("css", mod.Data.CSS); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.CSS = mod.Data.CSS
|
||||||
|
return http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Update the static generator options.
|
// Update the static generator options.
|
||||||
if mod.Which == "staticGen" {
|
if mod.Which == "staticGen" {
|
||||||
err = mapstructure.Decode(mod.Data.StaticGen, c.StaticGen)
|
err = mapstructure.Decode(mod.Data.StaticGen, c.StaticGen)
|
||||||
@@ -119,7 +134,7 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.db.Set("staticgen", c.staticgen, c.StaticGen)
|
err = c.Store.Config.Save("staticgen_"+c.StaticGen.Name(), c.StaticGen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -8,18 +8,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asdine/storm"
|
fm "github.com/hacdias/filemanager"
|
||||||
"github.com/asdine/storm/q"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type shareLink struct {
|
func shareHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
Hash string `json:"hash" storm:"id,index"`
|
|
||||||
Path string `json:"path" storm:"index"`
|
|
||||||
Expires bool `json:"expires"`
|
|
||||||
ExpireDate time.Time `json:"expireDate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
r.URL.Path = sanitizeURL(r.URL.Path)
|
r.URL.Path = sanitizeURL(r.URL.Path)
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
@@ -34,14 +26,10 @@ func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
|
|||||||
return http.StatusNotImplemented, nil
|
return http.StatusNotImplemented, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
var (
|
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||||
s []*shareLink
|
s, err := c.Store.Share.GetByPath(path)
|
||||||
path = filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
if err == fm.ErrNotExist {
|
||||||
)
|
|
||||||
|
|
||||||
err := c.db.Find("Path", path, &s)
|
|
||||||
if err == storm.ErrNotFound {
|
|
||||||
return http.StatusNotFound, nil
|
return http.StatusNotFound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,37 +39,42 @@ func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
for i, link := range s {
|
for i, link := range s {
|
||||||
if link.Expires && link.ExpireDate.Before(time.Now()) {
|
if link.Expires && link.ExpireDate.Before(time.Now()) {
|
||||||
c.db.DeleteStruct(&shareLink{Hash: link.Hash})
|
c.Store.Share.Delete(link.Hash)
|
||||||
s = append(s[:i], s[i+1:]...)
|
s = append(s[:i], s[i+1:]...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(s) == 0 {
|
||||||
|
return http.StatusNotFound, nil
|
||||||
|
}
|
||||||
|
|
||||||
return renderJSON(w, s)
|
return renderJSON(w, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||||
|
|
||||||
var s shareLink
|
var s *fm.ShareLink
|
||||||
expire := r.URL.Query().Get("expires")
|
expire := r.URL.Query().Get("expires")
|
||||||
unit := r.URL.Query().Get("unit")
|
unit := r.URL.Query().Get("unit")
|
||||||
|
|
||||||
if expire == "" {
|
if expire == "" {
|
||||||
err := c.db.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&s)
|
var err error
|
||||||
|
s, err = c.Store.Share.GetPermanent(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
|
w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes, err := generateRandomBytes(32)
|
bytes, err := fm.GenerateRandomBytes(32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
str := hex.EncodeToString(bytes)
|
str := hex.EncodeToString(bytes)
|
||||||
|
|
||||||
s = shareLink{
|
s = &fm.ShareLink{
|
||||||
Path: path,
|
Path: path,
|
||||||
Hash: str,
|
Hash: str,
|
||||||
Expires: expire != "",
|
Expires: expire != "",
|
||||||
@@ -108,19 +101,16 @@ func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
s.ExpireDate = time.Now().Add(add)
|
s.ExpireDate = time.Now().Add(add)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.db.Save(&s)
|
if err := c.Store.Share.Save(s); err != nil {
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderJSON(w, s)
|
return renderJSON(w, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
var s shareLink
|
s, err := c.Store.Share.Get(strings.TrimPrefix(r.URL.Path, "/"))
|
||||||
|
if err == fm.ErrNotExist {
|
||||||
err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)
|
|
||||||
if err == storm.ErrNotFound {
|
|
||||||
return http.StatusNotFound, nil
|
return http.StatusNotFound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +118,7 @@ func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.db.DeleteStruct(&s)
|
err = c.Store.Share.Delete(s.Hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/asdine/storm"
|
fm "github.com/hacdias/filemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
type modifyRequest struct {
|
type modifyRequest struct {
|
||||||
@@ -19,12 +19,12 @@ type modifyRequest struct {
|
|||||||
|
|
||||||
type modifyUserRequest struct {
|
type modifyUserRequest struct {
|
||||||
*modifyRequest
|
*modifyRequest
|
||||||
Data *User `json:"data"`
|
Data *fm.User `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// usersHandler is the entry point of the users API. It's just a router
|
// usersHandler is the entry point of the users API. It's just a router
|
||||||
// to send the request to its
|
// to send the request to its
|
||||||
func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func usersHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// If the user isn't admin and isn't making a PUT
|
// If the user isn't admin and isn't making a PUT
|
||||||
// request, then return forbidden.
|
// request, then return forbidden.
|
||||||
if !c.User.Admin && r.Method != http.MethodPut {
|
if !c.User.Admin && r.Method != http.MethodPut {
|
||||||
@@ -47,7 +47,7 @@ func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
|
|||||||
|
|
||||||
// getUserID returns the id from the user which is present
|
// getUserID returns the id from the user which is present
|
||||||
// in the request url. If the url is invalid and doesn't
|
// in the request url. If the url is invalid and doesn't
|
||||||
// contain a valid ID, it returns an error.
|
// contain a valid ID, it returns an fm.Error.
|
||||||
func getUserID(r *http.Request) (int, error) {
|
func getUserID(r *http.Request) (int, error) {
|
||||||
// Obtains the ID in string from the URL and converts
|
// Obtains the ID in string from the URL and converts
|
||||||
// it into an integer.
|
// it into an integer.
|
||||||
@@ -63,11 +63,11 @@ func getUserID(r *http.Request) (int, error) {
|
|||||||
|
|
||||||
// getUser returns the user which is present in the request
|
// getUser returns the user which is present in the request
|
||||||
// body. If the body is empty or the JSON is invalid, it
|
// body. If the body is empty or the JSON is invalid, it
|
||||||
// returns an error.
|
// returns an fm.Error.
|
||||||
func getUser(r *http.Request) (*User, string, error) {
|
func getUser(c *fm.Context, r *http.Request) (*fm.User, string, error) {
|
||||||
// Checks if the request body is empty.
|
// Checks if the request body is empty.
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return nil, "", errEmptyRequest
|
return nil, "", fm.ErrEmptyRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses the request body and checks if it's well formed.
|
// Parses the request body and checks if it's well formed.
|
||||||
@@ -79,13 +79,14 @@ func getUser(r *http.Request) (*User, string, error) {
|
|||||||
|
|
||||||
// Checks if the request type is right.
|
// Checks if the request type is right.
|
||||||
if mod.What != "user" {
|
if mod.What != "user" {
|
||||||
return nil, "", errWrongDataType
|
return nil, "", fm.ErrWrongDataType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod.Data.FileSystem = c.NewFS(mod.Data.Scope)
|
||||||
return mod.Data, mod.Which, nil
|
return mod.Data, mod.Which, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func usersGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// Request for the default user data.
|
// Request for the default user data.
|
||||||
if r.URL.Path == "/base" {
|
if r.URL.Path == "/base" {
|
||||||
return renderJSON(w, c.DefaultUser)
|
return renderJSON(w, c.DefaultUser)
|
||||||
@@ -93,15 +94,15 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// Request for the listing of users.
|
// Request for the listing of users.
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
users := []User{}
|
users, err := c.Store.Users.Gets(c.NewFS)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
for _, user := range c.Users {
|
for _, u := range users {
|
||||||
// Copies the user info and removes its
|
// Removes the user password so it won't
|
||||||
// password so it won't be sent to the
|
// be sent to the front-end.
|
||||||
// front-end.
|
|
||||||
u := *user
|
|
||||||
u.Password = ""
|
u.Password = ""
|
||||||
users = append(users, u)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(users, func(i, j int) bool {
|
sort.Slice(users, func(i, j int) bool {
|
||||||
@@ -116,54 +117,47 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Searches for the user and prints the one who matches.
|
u, err := c.Store.Users.Get(id, c.NewFS)
|
||||||
for _, user := range c.Users {
|
if err == fm.ErrExist {
|
||||||
if user.ID != id {
|
return http.StatusNotFound, err
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
u := *user
|
|
||||||
u.Password = ""
|
|
||||||
return renderJSON(w, u)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there aren't any matches, return not found.
|
if err != nil {
|
||||||
return http.StatusNotFound, errUserNotExist
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Password = ""
|
||||||
|
return renderJSON(w, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
return http.StatusMethodNotAllowed, nil
|
return http.StatusMethodNotAllowed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
u, _, err := getUser(r)
|
u, _, err := getUser(c, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusBadRequest, err
|
return http.StatusBadRequest, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if username isn't empty.
|
// Checks if username isn't empty.
|
||||||
if u.Username == "" {
|
if u.Username == "" {
|
||||||
return http.StatusBadRequest, errEmptyUsername
|
return http.StatusBadRequest, fm.ErrEmptyUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if filesystem isn't empty.
|
// Checks if scope isn't empty.
|
||||||
if u.FileSystem == "" {
|
if u.Scope == "" {
|
||||||
return http.StatusBadRequest, errEmptyScope
|
return http.StatusBadRequest, fm.ErrEmptyScope
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if password isn't empty.
|
// Checks if password isn't empty.
|
||||||
if u.Password == "" {
|
if u.Password == "" {
|
||||||
return http.StatusBadRequest, errEmptyPassword
|
return http.StatusBadRequest, fm.ErrEmptyPassword
|
||||||
}
|
|
||||||
|
|
||||||
// The username, password and scope cannot be empty.
|
|
||||||
if u.Username == "" || u.Password == "" || u.FileSystem == "" {
|
|
||||||
return http.StatusBadRequest, errors.New("username, password or scope is empty")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize rules if they're not initialized.
|
// Initialize rules if they're not initialized.
|
||||||
if u.Rules == nil {
|
if u.Rules == nil {
|
||||||
u.Rules = []*Rule{}
|
u.Rules = []*fm.Rule{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize commands if not initialized.
|
// Initialize commands if not initialized.
|
||||||
@@ -177,12 +171,12 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the scope exists.
|
// Checks if the scope exists.
|
||||||
if code, err := checkFS(string(u.FileSystem)); err != nil {
|
if code, err := checkFS(u.Scope); err != nil {
|
||||||
return code, err
|
return code, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hashes the password.
|
// Hashes the password.
|
||||||
pw, err := hashPassword(u.Password)
|
pw, err := fm.HashPassword(u.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -190,20 +184,17 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
u.Password = pw
|
u.Password = pw
|
||||||
|
|
||||||
// Saves the user to the database.
|
// Saves the user to the database.
|
||||||
err = c.db.Save(u)
|
err = c.Store.Users.Save(u)
|
||||||
if err == storm.ErrAlreadyExists {
|
if err == fm.ErrExist {
|
||||||
return http.StatusConflict, errUserExist
|
return http.StatusConflict, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saves the user to the memory.
|
|
||||||
c.Users[u.Username] = u
|
|
||||||
|
|
||||||
// Set the Location header and return.
|
// Set the Location header and return.
|
||||||
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
|
w.Header().Set("Location", "/settings/users/"+strconv.Itoa(u.ID))
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
@@ -231,7 +222,7 @@ func checkFS(path string) (int, error) {
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func usersDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
return http.StatusMethodNotAllowed, nil
|
return http.StatusMethodNotAllowed, nil
|
||||||
}
|
}
|
||||||
@@ -242,27 +233,19 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deletes the user from the database.
|
// Deletes the user from the database.
|
||||||
err = c.db.DeleteStruct(&User{ID: id})
|
err = c.Store.Users.Delete(id)
|
||||||
if err == storm.ErrNotFound {
|
if err == fm.ErrNotExist {
|
||||||
return http.StatusNotFound, errUserNotExist
|
return http.StatusNotFound, fm.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the user from the in-memory users map.
|
|
||||||
for _, user := range c.Users {
|
|
||||||
if user.ID == id {
|
|
||||||
delete(c.Users, user.Username)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// New users should be created on /api/users.
|
// New users should be created on /api/users.
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
return http.StatusMethodNotAllowed, nil
|
return http.StatusMethodNotAllowed, nil
|
||||||
@@ -280,21 +263,25 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gets the user from the request body.
|
// Gets the user from the request body.
|
||||||
u, which, err := getUser(r)
|
u, which, err := getUser(c, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusBadRequest, err
|
return http.StatusBadRequest, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're updating the default user. Only for NoAuth
|
||||||
|
// implementations. Used to change the viewMode.
|
||||||
|
if id == 0 && c.NoAuth {
|
||||||
|
c.DefaultUser.ViewMode = u.ViewMode
|
||||||
|
return http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Updates the CSS and locale.
|
// Updates the CSS and locale.
|
||||||
if which == "partial" {
|
if which == "partial" {
|
||||||
c.User.CSS = u.CSS
|
c.User.CSS = u.CSS
|
||||||
c.User.Locale = u.Locale
|
c.User.Locale = u.Locale
|
||||||
err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
|
c.User.ViewMode = u.ViewMode
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale)
|
err = c.Store.Users.Update(c.User, "CSS", "Locale", "ViewMode")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -305,16 +292,19 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
// Updates the Password.
|
// Updates the Password.
|
||||||
if which == "password" {
|
if which == "password" {
|
||||||
if u.Password == "" {
|
if u.Password == "" {
|
||||||
return http.StatusBadRequest, errEmptyPassword
|
return http.StatusBadRequest, fm.ErrEmptyPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
pw, err := hashPassword(u.Password)
|
if id == c.User.ID && c.User.LockPassword {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.User.Password, err = fm.HashPassword(u.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.User.Password = pw
|
err = c.Store.Users.Update(c.User, "Password")
|
||||||
err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -324,27 +314,27 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// If can only be all.
|
// If can only be all.
|
||||||
if which != "all" {
|
if which != "all" {
|
||||||
return http.StatusBadRequest, errInvalidUpdateField
|
return http.StatusBadRequest, fm.ErrInvalidUpdateField
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if username isn't empty.
|
// Checks if username isn't empty.
|
||||||
if u.Username == "" {
|
if u.Username == "" {
|
||||||
return http.StatusBadRequest, errEmptyUsername
|
return http.StatusBadRequest, fm.ErrEmptyUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if filesystem isn't empty.
|
// Checks if filesystem isn't empty.
|
||||||
if u.FileSystem == "" {
|
if u.Scope == "" {
|
||||||
return http.StatusBadRequest, errEmptyScope
|
return http.StatusBadRequest, fm.ErrEmptyScope
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the scope exists.
|
// Checks if the scope exists.
|
||||||
if code, err := checkFS(string(u.FileSystem)); err != nil {
|
if code, err := checkFS(u.Scope); err != nil {
|
||||||
return code, err
|
return code, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize rules if they're not initialized.
|
// Initialize rules if they're not initialized.
|
||||||
if u.Rules == nil {
|
if u.Rules == nil {
|
||||||
u.Rules = []*Rule{}
|
u.Rules = []*fm.Rule{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize commands if not initialized.
|
// Initialize commands if not initialized.
|
||||||
@@ -353,22 +343,20 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gets the current saved user from the in-memory map.
|
// Gets the current saved user from the in-memory map.
|
||||||
var suser *User
|
suser, err := c.Store.Users.Get(id, c.NewFS)
|
||||||
for _, user := range c.Users {
|
if err == fm.ErrNotExist {
|
||||||
if user.ID == id {
|
|
||||||
suser = user
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if suser == nil {
|
|
||||||
return http.StatusNotFound, nil
|
return http.StatusNotFound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
u.ID = id
|
u.ID = id
|
||||||
|
|
||||||
// Changes the password if the request wants it.
|
// Changes the password if the request wants it.
|
||||||
if u.Password != "" {
|
if u.Password != "" {
|
||||||
pw, err := hashPassword(u.Password)
|
pw, err := fm.HashPassword(u.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -380,17 +368,10 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// Updates the whole User struct because we always are supposed
|
// Updates the whole User struct because we always are supposed
|
||||||
// to send a new entire object.
|
// to send a new entire object.
|
||||||
err = c.db.Save(u)
|
err = c.Store.Users.Update(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user changed the username, delete the old user
|
|
||||||
// from the in-memory user map.
|
|
||||||
if suser.Username != u.Username {
|
|
||||||
delete(c.Users, suser.Username)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Users[u.Username] = u
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package filemanager
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
@@ -26,8 +27,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// command handles the requests for VCS related commands: git, svn and mercurial
|
// command handles the requests for VCS related commands: git, svn and mercurial
|
||||||
func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// Upgrades the connection to a websocket and checks for errors.
|
// Upgrades the connection to a websocket and checks for fm.Errors.
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -81,7 +82,7 @@ func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gets the path and initializes a buffer.
|
// Gets the path and initializes a buffer.
|
||||||
path := string(c.User.FileSystem) + "/" + r.URL.Path
|
path := c.User.Scope + "/" + r.URL.Path
|
||||||
path = filepath.Clean(path)
|
path = filepath.Clean(path)
|
||||||
buff := new(bytes.Buffer)
|
buff := new(bytes.Buffer)
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, er
|
|||||||
cmd.Stderr = buff
|
cmd.Stderr = buff
|
||||||
cmd.Stdout = buff
|
cmd.Stdout = buff
|
||||||
|
|
||||||
// Starts the command and checks for errors.
|
// Starts the command and checks for fm.Errors.
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
@@ -239,8 +240,8 @@ func parseSearch(value string) *searchOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// search searches for a file or directory.
|
// search searches for a file or directory.
|
||||||
func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// Upgrades the connection to a websocket and checks for errors.
|
// Upgrades the connection to a websocket and checks for fm.Errors.
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -269,7 +270,7 @@ func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, err
|
|||||||
search = parseSearch(value)
|
search = parseSearch(value)
|
||||||
scope := strings.TrimPrefix(r.URL.Path, "/")
|
scope := strings.TrimPrefix(r.URL.Path, "/")
|
||||||
scope = "/" + scope
|
scope = "/" + scope
|
||||||
scope = string(c.User.FileSystem) + scope
|
scope = c.User.Scope + scope
|
||||||
scope = strings.Replace(scope, "\\", "/", -1)
|
scope = strings.Replace(scope, "\\", "/", -1)
|
||||||
scope = filepath.Clean(scope)
|
scope = filepath.Clean(scope)
|
||||||
|
|
||||||
9161
package-lock.json
generated
Normal file
9161
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
|||||||
"filesize": "^3.5.10",
|
"filesize": "^3.5.10",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
"normalize.css": "^7.0.0",
|
"normalize.css": "^7.0.0",
|
||||||
|
"noty": "^3.1.2",
|
||||||
"vue": "^2.3.3",
|
"vue": "^2.3.3",
|
||||||
"vue-i18n": "^7.1.0",
|
"vue-i18n": "^7.1.0",
|
||||||
"vue-router": "^2.7.0",
|
"vue-router": "^2.7.0",
|
||||||
|
|||||||
24
publish.sh
Normal file
24
publish.sh
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Building assets"
|
||||||
|
./build.sh
|
||||||
|
|
||||||
|
echo "Updating version number to $1..."
|
||||||
|
sed -i "s|(untracked)|$1|g" filemanager.go
|
||||||
|
|
||||||
|
echo "Commiting..."
|
||||||
|
git add -A
|
||||||
|
git commit -m "Version $1"
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo "Creating the tag..."
|
||||||
|
git tag "v$1"
|
||||||
|
git push --tags
|
||||||
|
|
||||||
|
echo "Commiting untracked version notice..."
|
||||||
|
sed -i "s|$1|(untracked)|g" filemanager.go
|
||||||
|
git add -A
|
||||||
|
git commit -m "[ci skip] auto: setting untracked version"
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo "Done!"
|
||||||
@@ -1 +1 @@
|
|||||||
4b205fed13570c943ad67f9fc4db4f51aeb62cec
|
fb04f17b945f280665365cf87b85c4f8c936b31a
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package filemanager
|
package staticgen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
"github.com/hacdias/varutils"
|
"github.com/hacdias/varutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,15 +18,6 @@ var (
|
|||||||
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
|
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
|
||||||
)
|
)
|
||||||
|
|
||||||
// StaticGen is a static website generator.
|
|
||||||
type StaticGen interface {
|
|
||||||
SettingsPath() string
|
|
||||||
|
|
||||||
Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
|
||||||
Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
|
||||||
Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hugo is the Hugo static website generator.
|
// Hugo is the Hugo static website generator.
|
||||||
type Hugo struct {
|
type Hugo struct {
|
||||||
// Website root
|
// Website root
|
||||||
@@ -66,8 +58,13 @@ func (h Hugo) SettingsPath() string {
|
|||||||
return "/config." + frontmatter
|
return "/config." + frontmatter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name is the plugin's name.
|
||||||
|
func (h Hugo) Name() string {
|
||||||
|
return "hugo"
|
||||||
|
}
|
||||||
|
|
||||||
// Hook is the pre-api handler.
|
// Hook is the pre-api handler.
|
||||||
func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func (h Hugo) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// If we are not using HTTP Post, we shall return Method Not Allowed
|
// If we are not using HTTP Post, we shall return Method Not Allowed
|
||||||
// since we are only working with this method.
|
// since we are only working with this method.
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
@@ -87,7 +84,7 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
|
|||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
filename := filepath.Join(c.User.Scope, r.URL.Path)
|
||||||
archetype := r.Header.Get("archetype")
|
archetype := r.Header.Get("archetype")
|
||||||
|
|
||||||
ext := filepath.Ext(filename)
|
ext := filepath.Ext(filename)
|
||||||
@@ -110,8 +107,8 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Publish publishes a post.
|
// Publish publishes a post.
|
||||||
func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
filename := filepath.Join(c.User.Scope, r.URL.Path)
|
||||||
|
|
||||||
// We only run undraft command if it is a file.
|
// We only run undraft command if it is a file.
|
||||||
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
|
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
|
||||||
@@ -127,7 +124,7 @@ func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preview handles the preview path.
|
// Preview handles the preview path.
|
||||||
func (h *Hugo) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// Get a new temporary path if there is none.
|
// Get a new temporary path if there is none.
|
||||||
if h.previewPath == "" {
|
if h.previewPath == "" {
|
||||||
path, err := ioutil.TempDir("", "")
|
path, err := ioutil.TempDir("", "")
|
||||||
@@ -186,7 +183,8 @@ func (h Hugo) undraft(file string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hugo) find() error {
|
// Setup sets up the plugin.
|
||||||
|
func (h *Hugo) Setup() error {
|
||||||
var err error
|
var err error
|
||||||
if h.Exe, err = exec.LookPath("hugo"); err != nil {
|
if h.Exe, err = exec.LookPath("hugo"); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -194,114 +192,3 @@ func (h *Hugo) find() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jekyll is the Jekyll static website generator.
|
|
||||||
type Jekyll struct {
|
|
||||||
// Website root
|
|
||||||
Root string `name:"Website Root"`
|
|
||||||
// Public folder
|
|
||||||
Public string `name:"Public Directory"`
|
|
||||||
// Jekyll executable path
|
|
||||||
Exe string `name:"Executable"`
|
|
||||||
// Jekyll arguments
|
|
||||||
Args []string `name:"Arguments"`
|
|
||||||
// Indicates if we should clean public before a new publish.
|
|
||||||
CleanPublic bool `name:"Clean Public"`
|
|
||||||
// previewPath is the temporary path for a preview
|
|
||||||
previewPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SettingsPath retrieves the correct settings path.
|
|
||||||
func (j Jekyll) SettingsPath() string {
|
|
||||||
return "/_config.yml"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook is the pre-api handler.
|
|
||||||
func (j Jekyll) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish publishes a post.
|
|
||||||
func (j Jekyll) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
|
||||||
|
|
||||||
// We only run undraft command if it is a file.
|
|
||||||
if err := j.undraft(filename); err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regenerates the file
|
|
||||||
j.run()
|
|
||||||
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preview handles the preview path.
|
|
||||||
func (j *Jekyll) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
// Get a new temporary path if there is none.
|
|
||||||
if j.previewPath == "" {
|
|
||||||
path, err := ioutil.TempDir("", "")
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
j.previewPath = path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the arguments to execute Hugo: change the base URL,
|
|
||||||
// build the drafts and update the destination.
|
|
||||||
args := j.Args
|
|
||||||
args = append(args, "--baseurl", c.RootURL()+"/preview/")
|
|
||||||
args = append(args, "--drafts")
|
|
||||||
args = append(args, "--destination", j.previewPath)
|
|
||||||
|
|
||||||
// Builds the preview.
|
|
||||||
if err := runCommand(j.Exe, args, j.Root); err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serves the temporary path with the preview.
|
|
||||||
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j Jekyll) run() {
|
|
||||||
// If the CleanPublic option is enabled, clean it.
|
|
||||||
if j.CleanPublic {
|
|
||||||
os.RemoveAll(j.Public)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j Jekyll) undraft(file string) error {
|
|
||||||
if !strings.Contains(file, "_drafts") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *Jekyll) find() error {
|
|
||||||
var err error
|
|
||||||
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// runCommand executes an external command
|
|
||||||
func runCommand(command string, args []string, path string) error {
|
|
||||||
cmd := exec.Command(command, args...)
|
|
||||||
cmd.Dir = path
|
|
||||||
out, err := cmd.CombinedOutput()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(string(out))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
125
staticgen/jekyll.go
Normal file
125
staticgen/jekyll.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package staticgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
fm "github.com/hacdias/filemanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Jekyll is the Jekyll static website generator.
|
||||||
|
type Jekyll struct {
|
||||||
|
// Website root
|
||||||
|
Root string `name:"Website Root"`
|
||||||
|
// Public folder
|
||||||
|
Public string `name:"Public Directory"`
|
||||||
|
// Jekyll executable path
|
||||||
|
Exe string `name:"Executable"`
|
||||||
|
// Jekyll arguments
|
||||||
|
Args []string `name:"Arguments"`
|
||||||
|
// Indicates if we should clean public before a new publish.
|
||||||
|
CleanPublic bool `name:"Clean Public"`
|
||||||
|
// previewPath is the temporary path for a preview
|
||||||
|
previewPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name is the plugin's name.
|
||||||
|
func (j Jekyll) Name() string {
|
||||||
|
return "jekyll"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettingsPath retrieves the correct settings path.
|
||||||
|
func (j Jekyll) SettingsPath() string {
|
||||||
|
return "/_config.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook is the pre-api handler.
|
||||||
|
func (j Jekyll) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish publishes a post.
|
||||||
|
func (j Jekyll) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
filename := filepath.Join(c.User.Scope, r.URL.Path)
|
||||||
|
|
||||||
|
// We only run undraft command if it is a file.
|
||||||
|
if err := j.undraft(filename); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerates the file
|
||||||
|
j.run()
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview handles the preview path.
|
||||||
|
func (j *Jekyll) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
// Get a new temporary path if there is none.
|
||||||
|
if j.previewPath == "" {
|
||||||
|
path, err := ioutil.TempDir("", "")
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
j.previewPath = path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the arguments to execute Hugo: change the base URL,
|
||||||
|
// build the drafts and update the destination.
|
||||||
|
args := j.Args
|
||||||
|
args = append(args, "--baseurl", c.RootURL()+"/preview/")
|
||||||
|
args = append(args, "--drafts")
|
||||||
|
args = append(args, "--destination", j.previewPath)
|
||||||
|
|
||||||
|
// Builds the preview.
|
||||||
|
if err := runCommand(j.Exe, args, j.Root); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serves the temporary path with the preview.
|
||||||
|
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j Jekyll) run() {
|
||||||
|
// If the CleanPublic option is enabled, clean it.
|
||||||
|
if j.CleanPublic {
|
||||||
|
os.RemoveAll(j.Public)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j Jekyll) undraft(file string) error {
|
||||||
|
if !strings.Contains(file, "_drafts") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup sets up the plugin.
|
||||||
|
func (j *Jekyll) Setup() error {
|
||||||
|
var err error
|
||||||
|
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(j.Args) == 0 {
|
||||||
|
j.Args = []string{"build"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.Args[0] != "build" {
|
||||||
|
j.Args = append([]string{"build"}, j.Args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
19
staticgen/staticgen.go
Normal file
19
staticgen/staticgen.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package staticgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// runCommand executes an external command
|
||||||
|
func runCommand(command string, args []string, path string) error {
|
||||||
|
cmd := exec.Command(command, args...)
|
||||||
|
cmd.Dir = path
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
0
testdata/.gitkeep
vendored
0
testdata/.gitkeep
vendored
Reference in New Issue
Block a user