Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f986901e9 | ||
|
|
0b4d8df22d | ||
|
|
9619b4ee1d | ||
|
|
c75f3e0f59 | ||
|
|
6d71ab08ce | ||
|
|
121c695c1a | ||
|
|
6a1f9bbf69 | ||
|
|
ab54bc3c57 | ||
|
|
8a10baa815 | ||
|
|
98c22ece8d | ||
|
|
63247f6e8c | ||
|
|
58dd812008 | ||
|
|
1575fafec8 | ||
|
|
8c935eb51d | ||
|
|
7f3a26e1c0 | ||
|
|
fe834aa244 | ||
|
|
6620a1bf41 | ||
|
|
dab08325b7 | ||
|
|
ddb4685257 | ||
|
|
39dbca3b15 | ||
|
|
a02bdfc8d5 | ||
|
|
f88721cc83 | ||
|
|
95544bfed5 | ||
|
|
999ee1c2f5 | ||
|
|
a61ade62e5 | ||
|
|
08de5efeb4 | ||
|
|
74f690a71b | ||
|
|
1d4a3005ff | ||
|
|
7447a530ee | ||
|
|
fa9396f0f4 | ||
|
|
172bbb1828 | ||
|
|
410aa5d9da | ||
|
|
736cef7127 | ||
|
|
f4ceb7163e | ||
|
|
7bd4fbc0cb | ||
|
|
2ed4658369 | ||
|
|
735312982c | ||
|
|
4ddb3f5a34 | ||
|
|
c947228ac4 | ||
|
|
505af7d9d7 | ||
|
|
2c13ac4ac1 | ||
|
|
b5fbab2072 | ||
|
|
005b184d59 | ||
|
|
372bd813d9 | ||
|
|
52599314b0 | ||
|
|
f61e71e44f | ||
|
|
e5265b6632 | ||
|
|
003f361956 | ||
|
|
cc8369d83e | ||
|
|
aeb583a0bc | ||
|
|
41f07c64eb | ||
|
|
98588a66a7 | ||
|
|
49f32f876d | ||
|
|
aeb0f37e20 | ||
|
|
59a0daa293 | ||
|
|
c716d126eb | ||
|
|
5dc210c000 | ||
|
|
3bb3fdb956 | ||
|
|
b96a4a63c0 | ||
|
|
3d54b2bd90 | ||
|
|
95d43a344c | ||
|
|
1077d5cd6b | ||
|
|
57cc174b3d | ||
|
|
cf61baa273 | ||
|
|
2d5cd2d1d3 | ||
|
|
9472aad877 | ||
|
|
dd0f3ef144 | ||
|
|
44e492160b | ||
|
|
0137b03887 | ||
|
|
92c9b134c3 | ||
|
|
1737702c7c | ||
|
|
ece52ecf7c | ||
|
|
a7d6a72718 | ||
|
|
3fa9286238 | ||
|
|
ad5ff4cfe0 | ||
|
|
9aee1ebd2a | ||
|
|
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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,3 +6,7 @@ node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.idea
|
||||
.vscode
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
@@ -8,11 +8,17 @@ build:
|
||||
- freebsd
|
||||
- netbsd
|
||||
- openbsd
|
||||
- dragonfly
|
||||
- solaris
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
- mips
|
||||
- mips64
|
||||
- mipsle
|
||||
- mips64le
|
||||
ignore:
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
@@ -20,6 +26,8 @@ build:
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
|
||||
archive:
|
||||
name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
language: go
|
||||
|
||||
go: 1.8.3
|
||||
go: 1.x
|
||||
|
||||
env:
|
||||
- "PATH=/home/travis/gopath/bin:$PATH"
|
||||
@@ -15,7 +15,7 @@ install:
|
||||
- go get github.com/tsenart/deadcode
|
||||
|
||||
script:
|
||||
- gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --exclude="rice-box.go" --tests ./...
|
||||
- gometalinter --disable-all -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --exclude="rice-box.go" --tests ./...
|
||||
- go test ./... -timeout 30s
|
||||
|
||||
after_success:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
If you want to contribute or want to build the code from source, you will need to have the most recent version of Go and, if you want to change the static assets (JS, CSS, ...), Node.js installed on your computer. To start developing, you just need to do the following:
|
||||
|
||||
1. `go get github.com/hacdias/filemanager`
|
||||
1. `go get github.com/hacdias/filemanager/cmd/filemanager`
|
||||
2. `cd $GOPATH/src/github.com/hacdias/filemanager`
|
||||
3. `npm install`
|
||||
4. `npm run dev` - regenerates the static assets automatically
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"port": 80,
|
||||
"address": "",
|
||||
"database": "/etc/database.db",
|
||||
"database": "/database.db",
|
||||
"scope": "/srv",
|
||||
"allowCommands": true,
|
||||
"allowEdit": true,
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -7,16 +7,17 @@ RUN apk add --no-cache git
|
||||
RUN go get ./...
|
||||
|
||||
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
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=0 /go/bin/filemanager /usr/local/bin/filemanager
|
||||
FROM scratch
|
||||
COPY --from=0 /go/bin/filemanager /filemanager
|
||||
|
||||
VOLUME /tmp
|
||||
VOLUME /srv
|
||||
EXPOSE 80
|
||||
|
||||
COPY Docker.json /etc/config.json
|
||||
COPY Docker.json /config.json
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/filemanager"]
|
||||
CMD ["--config", "/etc/config.json"]
|
||||
ENTRYPOINT ["/filemanager"]
|
||||
CMD ["--config", "/config.json"]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[](https://travis-ci.org/hacdias/filemanager)
|
||||
[](https://goreportcard.com/report/hacdias/filemanager)
|
||||
[](http://godoc.org/github.com/hacdias/filemanager)
|
||||
[](https://github.com/hacdias/filemanager/releases/latest)
|
||||
|
||||
filemanager provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app or as a middleware.
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<meta name="base" content="{{ .BaseURL }}">
|
||||
<meta name="staticgen" content="{{ .StaticGen }}">
|
||||
<meta name="noauth" content="{{ .NoAuth }}">
|
||||
<meta name="version" content="{{ .Version }}">
|
||||
<meta name="recaptcha" content="{{ .ReCaptchaKey }}">
|
||||
<title>File Manager</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
|
||||
@@ -22,7 +25,14 @@
|
||||
<!-- Add to home screen for Windows -->
|
||||
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
|
||||
<meta name="msapplication-TileColor" content="#2979ff">
|
||||
<% for (var chunk of webpack.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) {
|
||||
if (file.match(/\.(js|css)$/)) { %>
|
||||
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
<router-view :dependencies="loaded" @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
mounted: function () {
|
||||
computed: mapState(['recaptcha']),
|
||||
data () {
|
||||
return {
|
||||
loaded: false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.recaptcha.length === 0) {
|
||||
this.unload()
|
||||
return
|
||||
}
|
||||
|
||||
let check = () => {
|
||||
if (typeof window.grecaptcha === 'undefined') {
|
||||
setTimeout(check, 100)
|
||||
return
|
||||
}
|
||||
|
||||
this.unload()
|
||||
}
|
||||
|
||||
check()
|
||||
},
|
||||
methods: {
|
||||
unload () {
|
||||
this.loaded = true
|
||||
// Remove loading animation.
|
||||
let loading = document.getElementById('loading')
|
||||
loading.classList.add('done')
|
||||
@@ -13,6 +40,35 @@ export default {
|
||||
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>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="selected">
|
||||
<option value="en">{{ $t('languages.en') }}</option>
|
||||
<option value="fr">{{ $t('languages.fr') }}</option>
|
||||
<option value="pt">{{ $t('languages.pt') }}</option>
|
||||
<option value="ja">{{ $t('languages.ja') }}</option>
|
||||
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
||||
<option value="zh-tw">{{ $t('languages.zhTW') }}</option>
|
||||
<option value="es">{{ $t('languages.es') }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</button>
|
||||
</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')">
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>{{ $t('sidebar.settings') }}</span>
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</p>
|
||||
</nav>
|
||||
@@ -72,7 +72,7 @@ import auth from '@/utils/auth'
|
||||
export default {
|
||||
name: 'sidebar',
|
||||
computed: {
|
||||
...mapState(['user', 'staticGen']),
|
||||
...mapState(['user', 'staticGen', 'version']),
|
||||
active () {
|
||||
return this.$store.state.show === 'sidebar'
|
||||
}
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
<template>
|
||||
<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>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { updateUser } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'switch-button',
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
icon: function () {
|
||||
if (this.user.viewMode === 'mosaic') return 'view_list'
|
||||
return 'view_module'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['updateUser']),
|
||||
change: function (event) {
|
||||
// If we are on mobile we should close the dropdown.
|
||||
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') {
|
||||
display = 'list'
|
||||
}
|
||||
|
||||
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'
|
||||
updateUser(user, 'partial').then(() => {
|
||||
this.updateUser({ viewMode: user.viewMode })
|
||||
}).catch(this.$showError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done(button)
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
this.$store.commit('setSchedule', '')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="req.display"
|
||||
:class="user.viewMode"
|
||||
@dragenter="dragEnter"
|
||||
@dragend="dragEnd">
|
||||
<div>
|
||||
@@ -98,7 +98,7 @@ export default {
|
||||
name: 'listing',
|
||||
components: { Item },
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapState(['req', 'selected', 'user']),
|
||||
nameSorted () {
|
||||
return (this.req.sort === 'name')
|
||||
},
|
||||
@@ -196,6 +196,10 @@ export default {
|
||||
})
|
||||
},
|
||||
paste (event) {
|
||||
if (event.target.tagName.toLowerCase() === 'input') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
let items = []
|
||||
@@ -210,17 +214,13 @@ export default {
|
||||
if (this.$store.state.clipboard.key === 'x') {
|
||||
api.move(items).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
}).catch(this.$showError)
|
||||
return
|
||||
}
|
||||
|
||||
api.copy(items).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
}).catch(this.$showError)
|
||||
},
|
||||
resizeEvent () {
|
||||
// Update the columns size based on the window width.
|
||||
@@ -267,7 +267,7 @@ export default {
|
||||
.then(req => {
|
||||
this.checkConflict(files, req.items, base)
|
||||
})
|
||||
.catch(error => { console.log(error) })
|
||||
.catch(this.$showError)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -318,19 +318,37 @@ export default {
|
||||
handleFiles (files, base, overwrite = false) {
|
||||
buttons.loading('upload')
|
||||
let promises = []
|
||||
let progress = new Array(files.length).fill(0)
|
||||
|
||||
for (let file of files) {
|
||||
promises.push(api.post(this.$route.path + base + file.name, file, overwrite))
|
||||
let onupload = (id) => (event) => {
|
||||
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)
|
||||
.then(() => {
|
||||
buttons.success('upload')
|
||||
finish()
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('upload')
|
||||
this.$store.commit('showError', error)
|
||||
finish()
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
return false
|
||||
|
||||
@@ -109,21 +109,36 @@ export default {
|
||||
.then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
click: function (event) {
|
||||
if (this.selectedCount !== 0) event.preventDefault()
|
||||
if (this.$store.state.selected.indexOf(this.index) === -1) {
|
||||
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||
|
||||
this.addSelected(this.index)
|
||||
} else {
|
||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||
this.removeSelected(this.index)
|
||||
return
|
||||
}
|
||||
|
||||
return false
|
||||
if (event.shiftKey && this.selected.length === 1) {
|
||||
let fi = 0
|
||||
let la = 0
|
||||
|
||||
if (this.index > this.selected[0]) {
|
||||
fi = this.selected[0] + 1
|
||||
la = this.index
|
||||
} else {
|
||||
fi = this.index
|
||||
la = this.selected[0] - 1
|
||||
}
|
||||
|
||||
for (; fi <= la; fi++) {
|
||||
this.addSelected(fi)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
},
|
||||
touchstart (event) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
<div class="preview">
|
||||
<img v-if="req.type == 'image'" :src="raw()">
|
||||
<audio v-else-if="req.type == 'audio'" :src="raw()" controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw()" controls>
|
||||
<audio v-else-if="req.type == 'audio'" :src="raw()" autoplay controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw()" autoplay controls>
|
||||
Sorry, your browser doesn't support embedded videos,
|
||||
but don't worry, you can <a :href="download()">download it</a>
|
||||
and watch it with your favorite video player!
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
this.listing = req
|
||||
this.updateLinks()
|
||||
})
|
||||
.catch(error => { console.log(error) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keyup', this.key)
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.copy') }}</h3>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.copy') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.copyMessage') }}</p>
|
||||
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="copy"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="copy"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -56,7 +61,7 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('copy')
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.deleteTitle') }}</h3>
|
||||
<p v-show="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
||||
<p v-show="req.kind === 'listing'">{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
||||
<div>
|
||||
<button @click="submit"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
|
||||
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button @click="$store.commit('closeHovers')"
|
||||
class="flat cancel"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button @click="submit"
|
||||
class="flat"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,7 +45,7 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('delete')
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
return
|
||||
@@ -70,7 +72,7 @@ export default {
|
||||
.catch(error => {
|
||||
buttons.done('delete')
|
||||
this.$store.commit('setReload', true)
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="prompt" id="download">
|
||||
<h3>{{ $t('prompts.download') }}</h3>
|
||||
<div class="card floating" id="download">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.download') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.downloadMessage') }}</p>
|
||||
|
||||
<button @click="download('zip')" autofocus>zip</button>
|
||||
<button @click="download('tar')" autofocus>tar</button>
|
||||
<button @click="download('targz')" autofocus>tar.gz</button>
|
||||
<button @click="download('tarbz2')" autofocus>tar.bz2</button>
|
||||
<button @click="download('tarxz')" autofocus>tar.xz</button>
|
||||
<button class="block cancel" @click="download('zip')" autofocus>zip</button>
|
||||
<button class="block cancel" @click="download('tar')" autofocus>tar</button>
|
||||
<button class="block cancel" @click="download('targz')" autofocus>tar.gz</button>
|
||||
<button class="block cancel" @click="download('tarbz2')" autofocus>tar.bz2</button>
|
||||
<button class="block cancel" @click="download('tarxz')" autofocus>tar.xz</button>
|
||||
</div>
|
||||
</div>
|
||||
</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.
|
||||
api.fetch(url.removeLastDir(this.$route.path))
|
||||
.then(this.fillOptions)
|
||||
.catch(this.showError)
|
||||
.catch(this.$showError)
|
||||
},
|
||||
methods: {
|
||||
fillOptions (req) {
|
||||
@@ -96,7 +96,7 @@ export default {
|
||||
|
||||
api.fetch(uri)
|
||||
.then(this.fillOptions)
|
||||
.catch(this.showError)
|
||||
.catch(this.$showError)
|
||||
},
|
||||
touchstart (event) {
|
||||
let url = event.currentTarget.dataset.url
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div class="prompt help">
|
||||
<h3>{{ $t('help.help') }}</h3>
|
||||
<div class="card floating help">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('help.help') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
||||
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
||||
@@ -13,11 +16,12 @@
|
||||
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
||||
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="ok"
|
||||
class="flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.fileInfo') }}</h3>
|
||||
<div class="card floating">
|
||||
<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 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.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>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card-action">
|
||||
<button type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
class="ok"
|
||||
class="flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
@@ -111,7 +115,7 @@ export default {
|
||||
|
||||
api.checksum(link, hash)
|
||||
.then((hash) => { event.target.innerHTML = hash })
|
||||
.catch(error => { this.$store.commit('showError', error) })
|
||||
.catch(this.$showError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.move') }}</h3>
|
||||
<p>{{ $t('prompts.moveMessage') }}</p>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.move') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="move"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card-action">
|
||||
<button class="flat cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="move"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -56,7 +60,7 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
buttons.done('move')
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newArchetype') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<input type="text" @keyup.enter="submit" v-model.trim="archetype">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="flat cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -37,17 +43,19 @@ export default {
|
||||
.then((url) => {
|
||||
this.$router.push({ path: url })
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
},
|
||||
new (url, type) {
|
||||
url = removePrefix(url)
|
||||
|
||||
if (!url.endsWith('.md') && !url.endsWith('.markdown')) {
|
||||
url += '.markdown'
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
||||
if (!this.$store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
|
||||
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
||||
|
||||
request.onload = () => {
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.newDir') }}</h3>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newDir') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newDirMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
@click="submit">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
@click="submit">{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,7 +49,7 @@ export default {
|
||||
|
||||
api.post(uri)
|
||||
.then(() => { this.$router.push({ path: uri }) })
|
||||
.catch(error => { this.$store.commit('showError', error) })
|
||||
.catch(this.$showError)
|
||||
|
||||
// Close the prompt
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.newFile') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.newFileMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')">{{ $t('buttons.create') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -44,7 +50,7 @@ export default {
|
||||
// Create the new file.
|
||||
api.post(uri)
|
||||
.then(() => { this.$router.push({ path: uri }) })
|
||||
.catch(error => { this.$store.commit('showError', error) })
|
||||
.catch(this.$showError)
|
||||
|
||||
// Close the prompt.
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
<info v-else-if="showInfo"></info>
|
||||
<move v-else-if="showMove"></move>
|
||||
<copy v-else-if="showCopy"></copy>
|
||||
<error v-else-if="showError"></error>
|
||||
<success v-else-if="showSuccess"></success>
|
||||
<replace v-else-if="showReplace"></replace>
|
||||
<schedule v-else-if="show === 'schedule'"></schedule>
|
||||
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
|
||||
@@ -27,8 +25,6 @@ import Rename from './Rename'
|
||||
import Download from './Download'
|
||||
import Move from './Move'
|
||||
import Copy from './Copy'
|
||||
import Error from './Error'
|
||||
import Success from './Success'
|
||||
import NewFile from './NewFile'
|
||||
import NewDir from './NewDir'
|
||||
import NewArchetype from './NewArchetype'
|
||||
@@ -47,9 +43,7 @@ export default {
|
||||
NewArchetype,
|
||||
Schedule,
|
||||
Rename,
|
||||
Error,
|
||||
Download,
|
||||
Success,
|
||||
Move,
|
||||
Copy,
|
||||
Share,
|
||||
@@ -70,8 +64,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState(['show', 'plugins']),
|
||||
showError: function () { return this.show === 'error' },
|
||||
showSuccess: function () { return this.show === 'success' },
|
||||
showInfo: function () { return this.show === 'info' },
|
||||
showHelp: function () { return this.show === 'help' },
|
||||
showDelete: function () { return this.show === 'delete' },
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.rename') }}</h3>
|
||||
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.rename') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button @click="submit"
|
||||
type="submit"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||
<button class="cancel"
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button @click="submit"
|
||||
class="flat"
|
||||
type="submit"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -68,7 +74,7 @@ export default {
|
||||
}
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(error => {
|
||||
this.$store.commit('showError', error)
|
||||
this.$showError(error)
|
||||
})
|
||||
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.replace') }}</h3>
|
||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.replace') }}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="showConfirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||
<button class="cancel"
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="flat cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="flat"
|
||||
@click="showConfirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>{{ $t('prompts.schedule') }}</h3>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('prompts.schedule') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t('prompts.scheduleMessage') }}</p>
|
||||
<input autofocus type="datetime-local" v-model="date">
|
||||
<div>
|
||||
<button class="ok"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
|
||||
<button class="cancel"
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="close"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="falt"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.schedule')"
|
||||
:title="$t('buttons.schedule')">{{ $t('buttons.schedule') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div class="prompt" id="share">
|
||||
<h3>{{ $t('buttons.share') }}</h3>
|
||||
<p></p>
|
||||
<div class="card floating" id="share">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('buttons.share') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<ul>
|
||||
<li v-if="!hasPermanent">
|
||||
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
|
||||
@@ -18,7 +21,7 @@
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
|
||||
<button class="action copy"
|
||||
<button class="action copy-clipboard"
|
||||
:data-clipboard-text="buildLink(link.hash)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||
@@ -43,9 +46,10 @@
|
||||
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="cancel"
|
||||
<div class="card-action">
|
||||
<button class="flat"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
|
||||
@@ -54,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
import { getShare, deleteShare, share } from '@/utils/api'
|
||||
import moment from 'moment'
|
||||
import Clipboard from 'clipboard'
|
||||
@@ -101,20 +105,25 @@ export default {
|
||||
})
|
||||
.catch(error => {
|
||||
if (error === 404) return
|
||||
this.showError(error)
|
||||
this.$showError(error)
|
||||
})
|
||||
},
|
||||
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: {
|
||||
...mapMutations([ 'showError' ]),
|
||||
submit: function (event) {
|
||||
if (!this.time) return
|
||||
|
||||
share(this.url, this.time, this.unit)
|
||||
.then(result => { this.links.push(result); this.sort() })
|
||||
.catch(error => { this.showError(error) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
getPermalink (event) {
|
||||
share(this.url)
|
||||
@@ -123,7 +132,7 @@ export default {
|
||||
this.sort()
|
||||
this.hasPermanent = true
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
deleteLink (event, link) {
|
||||
event.preventDefault()
|
||||
@@ -132,7 +141,7 @@ export default {
|
||||
if (!link.expires) this.hasPermanent = false
|
||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
.catch(this.$showError)
|
||||
},
|
||||
humanTime (time) {
|
||||
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 {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
padding-top: 4em;
|
||||
background-color: #f8f8f8;
|
||||
user-select: none;
|
||||
color: #212121;
|
||||
background-color: #fafafa;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -66,6 +65,58 @@ button:hover {
|
||||
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;
|
||||
}
|
||||
|
||||
button.flat[disabled] {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -145,3 +196,19 @@ main {
|
||||
#breadcrumbs span a {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
.dashboard a {
|
||||
a {
|
||||
color: inherit
|
||||
}
|
||||
|
||||
.dashboard h1 button {
|
||||
font-size: 0.5em;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.dashboard table {
|
||||
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;
|
||||
select,
|
||||
textarea,
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
padding: 0.5em 0;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
transition: .2s ease border;
|
||||
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,
|
||||
@@ -69,49 +39,32 @@ form.dashboard > p:last-child {
|
||||
}
|
||||
|
||||
.dashboard #locale {
|
||||
border: 1px solid #dddddd;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
.dashboard textarea:focus,
|
||||
.dashboard textarea:hover,
|
||||
.dashboard input[type="text"]:focus,
|
||||
.dashboard input[type="password"]:focus,
|
||||
.dashboard input[type="text"]:hover,
|
||||
.dashboard input[type="password"]:hover {
|
||||
textarea:focus,
|
||||
textarea:hover,
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="text"]:hover,
|
||||
input[type="password"]:hover {
|
||||
border-color: #2979ff;
|
||||
}
|
||||
|
||||
.dashboard input.red {
|
||||
input.red {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
.dashboard input.green {
|
||||
input.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 {
|
||||
margin-bottom: .2em;
|
||||
display: block;
|
||||
font-size: .8em;
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.57);
|
||||
}
|
||||
|
||||
li code,
|
||||
@@ -131,21 +84,337 @@ p code {
|
||||
display: flex;
|
||||
color: rgb(84, 110, 122);
|
||||
font-weight: 500;
|
||||
padding: 0 0 1em;
|
||||
margin: 0 0 1em;
|
||||
font-size: .8em;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
text-align: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard #nav li {
|
||||
width: 100%;
|
||||
padding: 0 0 1em;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dashboard #nav li:last-child {
|
||||
text-align: right
|
||||
.dashboard #nav li.active {
|
||||
border-color: #2196f3
|
||||
}
|
||||
|
||||
.dashboard #nav i {
|
||||
font-size: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table tr {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
table tr:last-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
table th {
|
||||
font-weight: 500;
|
||||
color: #757575;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: .5em 0;
|
||||
}
|
||||
|
||||
table td.small {
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
table tr>*:first-child {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
table tr>*:last-child {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
margin: .5rem 0 1rem 0;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card.floating {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 99999;
|
||||
max-width: 25em;
|
||||
width: 90%;
|
||||
max-height: 95%;
|
||||
z-index: 99999;
|
||||
animation: .1s show forwards;
|
||||
}
|
||||
|
||||
.card>*>*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.card>*>*:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card .card-title {
|
||||
padding: 1.5em 1em 1em;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.card .card-title>*:first-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.card>div {
|
||||
padding: 1em 1em;
|
||||
}
|
||||
|
||||
.card>div:first-child {
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
|
||||
.card>div:last-child {
|
||||
padding-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.card .card-title * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card .card-action {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card .card-content.full {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
color: rgba(0, 0, 0, 0.53);
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
margin: 2em 0 1em;
|
||||
}
|
||||
|
||||
.card-content table {
|
||||
margin: 0 -1em;
|
||||
width: calc(100% + 2em);
|
||||
}
|
||||
|
||||
.card code {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.card#download {
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
.card#share ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card#share ul li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card#share ul li a {
|
||||
color: #2196F3;
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.card#share ul li .action i {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.card#share ul li input,
|
||||
.card#share ul li select {
|
||||
padding: .2em;
|
||||
margin-right: .5em;
|
||||
border: 1px solid #dadada;
|
||||
}
|
||||
|
||||
.card#share .action.copy-clipboard::after {
|
||||
content: 'Copied!';
|
||||
position: absolute;
|
||||
left: -25%;
|
||||
width: 150%;
|
||||
font-size: .6em;
|
||||
text-align: center;
|
||||
background: #44a6f5;
|
||||
color: #fff;
|
||||
padding: .5em .2em;
|
||||
border-radius: .4em;
|
||||
top: -2em;
|
||||
transition: .1s ease opacity;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.card#share .action.copy-clipboard.active::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
animation: .1s show forwards;
|
||||
}
|
||||
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* PROMPT - MOVE *
|
||||
* * * * * * * * * * * * * * * */
|
||||
|
||||
.file-list {
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-list li {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
border-radius: .2em;
|
||||
padding: .3em;
|
||||
}
|
||||
|
||||
.file-list li[aria-selected=true] {
|
||||
background: #2196f3 !important;
|
||||
color: #fff !important;
|
||||
transition: .1s ease all;
|
||||
}
|
||||
|
||||
.file-list li:hover {
|
||||
background-color: #e9eaeb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-list li:before {
|
||||
content: "folder";
|
||||
color: #6f6f6f;
|
||||
vertical-align: middle;
|
||||
line-height: 1.4;
|
||||
font-family: 'Material Icons';
|
||||
font-size: 1.75em;
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
.file-list li[aria-selected=true]:before {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.help {
|
||||
max-width: 24em;
|
||||
}
|
||||
|
||||
.help ul {
|
||||
padding: 0;
|
||||
margin: 1em 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@keyframes show {
|
||||
0% {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
1% {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible {
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.collapsible:last-of-type {
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.collapsible > input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsible > label {
|
||||
padding: 1em 0;
|
||||
cursor: pointer;
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.collapsible > label * {
|
||||
margin: 0;
|
||||
color: rgba(0,0,0,0.57);
|
||||
}
|
||||
|
||||
.collapsible > label i {
|
||||
transition: .2s ease transform;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapsible .collapse {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: .2s ease all;
|
||||
}
|
||||
|
||||
.collapsible > input:checked ~ .collapse {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
max-height: 20em;
|
||||
}
|
||||
|
||||
.collapsible > input:checked ~ label i {
|
||||
transform: rotate(180deg)
|
||||
}
|
||||
|
||||
.card .collapsible {
|
||||
width: calc(100% + 2em);
|
||||
margin: 0 -1em;
|
||||
}
|
||||
|
||||
.card .collapsible > label {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.card .collapsible .collapse {
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
@@ -122,8 +122,9 @@ header .search-button {
|
||||
#search input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background-color: transparent;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#search #result {
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
|
||||
#listing.list .item.header {
|
||||
display: flex !important;
|
||||
background: #f8f8f8;
|
||||
background: #fafafa;
|
||||
position: fixed;
|
||||
width: calc(100% - 19em);
|
||||
top: 7em;
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#login.recaptcha form {
|
||||
min-width: 304px;
|
||||
}
|
||||
|
||||
#login #recaptcha {
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
#login input {
|
||||
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 "~noty/lib/noty.css";
|
||||
@import "./fonts.css";
|
||||
@import "./base.css";
|
||||
@import "./header.css";
|
||||
@import "./prompts.css";
|
||||
@import "./listing.css";
|
||||
@import "./editor.css";
|
||||
@import "./dashboard.css";
|
||||
@@ -180,6 +180,17 @@
|
||||
* 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 *
|
||||
|
||||
@@ -26,11 +26,13 @@ buttons:
|
||||
publish: Publish
|
||||
selectMultiple: Select multiple
|
||||
schedule: Schedule
|
||||
switchView: Swicth view
|
||||
switchView: Switch view
|
||||
toggleSidebar: Toggle sidebar
|
||||
update: Update
|
||||
upload: Upload
|
||||
permalink: Get Permanent Link
|
||||
success:
|
||||
linkCopied: Link copied!
|
||||
errors:
|
||||
forbidden: You're not welcome here.
|
||||
internal: Something really went wrong.
|
||||
@@ -115,13 +117,14 @@ settings:
|
||||
commandsHelp: >
|
||||
Here you can set commands that are executed in the named events. You
|
||||
write one command per line. If the event is related to files, such as before and
|
||||
after saving, the environment variable "file" will be available with the path
|
||||
after saving, the environment variable "FILE" will be available with the path
|
||||
of the file.
|
||||
commandsUpdated: Commands updated!
|
||||
customStylesheet: Custom Stylesheet
|
||||
examples: Examples
|
||||
globalSettings: Global Settings
|
||||
language: Language
|
||||
lockPassword: Prevent the user from changing the password
|
||||
newPassword: Your new password
|
||||
newPasswordConfirm: Confirm your new password
|
||||
newUser: New User
|
||||
@@ -165,7 +168,6 @@ sidebar:
|
||||
myFiles: My files
|
||||
newFile: New file
|
||||
newFolder: New folder
|
||||
servedWith: Served with
|
||||
settings: Settings
|
||||
siteSettings: Site Settings
|
||||
hugoNew: Hugo New
|
||||
@@ -185,10 +187,12 @@ search:
|
||||
writeToSearch: Write here to search
|
||||
languages:
|
||||
en: English
|
||||
fr: Français
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
zhTW: 中文 (繁體)
|
||||
es: Español
|
||||
time:
|
||||
unit: Time Unit
|
||||
seconds: Seconds
|
||||
|
||||
202
assets/src/i18n/es.yaml
Normal file
202
assets/src/i18n/es.yaml
Normal file
@@ -0,0 +1,202 @@
|
||||
permanent: Permanente
|
||||
buttons:
|
||||
cancel: Cancelar
|
||||
close: Cerrar
|
||||
copy: Copiar
|
||||
copyFile: Copiar archivo
|
||||
copyToClipboard: Copiar al portapapeles
|
||||
create: Crear
|
||||
delete: Borrar
|
||||
download: Descargar
|
||||
info: Info
|
||||
more: Más
|
||||
move: Mover
|
||||
moveFile: Mover archivo
|
||||
new: Nuevo
|
||||
next: Siguiente
|
||||
ok: OK
|
||||
replace: Reemplazar
|
||||
previous: Anterior
|
||||
rename: Renombrar
|
||||
reportIssue: Reportar problema
|
||||
save: Guardar
|
||||
search: Buscar
|
||||
select: Seleccionar
|
||||
share: Compartir
|
||||
publish: Publicar
|
||||
selectMultiple: Selección múltiple
|
||||
schedule: Programar
|
||||
switchView: Cambiar vista
|
||||
toggleSidebar: Mostrar/Ocultar menú
|
||||
update: Actualizar
|
||||
upload: Subir
|
||||
permalink: Link permanente
|
||||
success:
|
||||
linkCopied: ¡Link copiado!
|
||||
errors:
|
||||
forbidden: No eres bienvenido aquí.
|
||||
internal: La verdad es que algo ha ido mal.
|
||||
notFound: No se puede acceder a este lugar.
|
||||
files:
|
||||
folders: Carpetas
|
||||
files: Archivos
|
||||
body: Cuerpo
|
||||
clear: Limpiar
|
||||
closePreview: Cerrar vista previa
|
||||
home: Inicio
|
||||
lastModified: Última modificación
|
||||
loading: Cargando...
|
||||
lonely: Uno se siente muy sólo aquí...
|
||||
metadata: Metadatos
|
||||
multipleSelectionEnabled: Selección múltiple activada
|
||||
name: Nombre
|
||||
size: Tamaño
|
||||
sortByName: Ordenar por nombre
|
||||
sortBySize: Ordenar por tamaño
|
||||
sortByLastModified: Ordenar por última modificación
|
||||
help:
|
||||
click: seleccionar archivo o carpeta
|
||||
ctrl:
|
||||
click: seleccionar múltiples archivos o carpetas
|
||||
f: abre la búsqueda
|
||||
s: guarda un archivo o lo descarga a la carpeta en la que estás
|
||||
del: elimina los items seleccionados
|
||||
doubleClick: abre un archivo o carpeta
|
||||
esc: limpia la selección y/o cierra la ventana
|
||||
f1: esta información
|
||||
f2: renombrar archivo
|
||||
help: Ayuda
|
||||
login:
|
||||
password: Contraseña
|
||||
submit: Iniciar sesión
|
||||
username: Usuario
|
||||
wrongCredentials: Usuario y/o contraseña incorrectos
|
||||
prompts:
|
||||
copy: Copiar
|
||||
copyMessage: 'Elige el lugar donde quieres copiar tus archivos:'
|
||||
currentlyNavigating: 'Actualmente estás en:'
|
||||
deleteMessageMultiple: ¿Estás seguro que quieres eliminar {count} archivo(s)?
|
||||
deleteMessageSingle: ¿Estás seguro que quieres eliminar este archivo/carpeta?
|
||||
deleteTitle: Borrar archivos
|
||||
displayName: 'Nombre:'
|
||||
download: Descargar archivos
|
||||
downloadMessage: Elige el formato de descarga.
|
||||
error: Algo ha fallado
|
||||
fileInfo: Información del archivo
|
||||
filesSelected: "{count} archivos seleccionados."
|
||||
lastModified: Última modificación
|
||||
move: Mover
|
||||
moveMessage: 'Elige una nueva casa para tus archivo(s)/carpeta(s):'
|
||||
newDir: Nueva carpeta
|
||||
newDirMessage: Escribe el nombre de la nueva carpeta.
|
||||
newFile: Nuevo archivo
|
||||
newFileMessage: Escribe el nombre del nuevo archivo.
|
||||
numberDirs: Número de carpetas
|
||||
numberFiles: Número de archivos
|
||||
replace: Reemplazar
|
||||
replaceMessage: >
|
||||
Uno de los archivos ue intentas subir está creando conflicto por su nombre.
|
||||
¿Quieres cambiar el nombre del ya existente?
|
||||
rename: Renombrar
|
||||
renameMessage: Escribe el nuevo nombre para
|
||||
show: Mostrar
|
||||
size: Tamaño
|
||||
schedule: Programar
|
||||
scheduleMessage: Elige una hora y fecha para programar la publicación de este post.
|
||||
newArchetype: Crea un nuevo post basado en un arquetipo. Tu archivo será creado en la carpeta de contenido.
|
||||
settings:
|
||||
admin: Admin
|
||||
administrator: Administrador
|
||||
allowCommands: Ejecutar comandos
|
||||
allowEdit: Editar, renombrar y borrar archivos o carpetas
|
||||
allowNew: Crear nuevos archivos y carpetas
|
||||
allowPublish: Publicar nuevos posts y páginas
|
||||
avoidChanges: "(dejar en blanco para evitar cambios)"
|
||||
changePassword: Cambiar contraseña
|
||||
commands: Comandos
|
||||
commandsHelp: >
|
||||
Aquí puedes crear comandos que serán ejecutados en los eventos. Debes
|
||||
escribir un comando por linea. Si el evento está relacionado con archivos, como
|
||||
por ejemplo, antes y después de guardar, la variable de entorno "FILE" estará
|
||||
disponible en la ruta del archivo.
|
||||
commandsUpdated: ¡Comandos actualizados!
|
||||
customStylesheet: Modificar hoja de estilos
|
||||
examples: Ejemplos
|
||||
globalSettings: Ajustes globales
|
||||
language: Idioma
|
||||
lockPassword: Evitar que el usuario cambie la contraseña
|
||||
newPassword: Tu nueva contraseña
|
||||
newPasswordConfirm: Confirma tu contraseña
|
||||
newUser: Nuevo usuario
|
||||
password: Contraseña
|
||||
passwordUpdated: ¡Contraseña actualizada!
|
||||
permissions: Permisos
|
||||
permissionsHelp: >
|
||||
Puedes nombrar al usuario como administrador o elegir los permisos
|
||||
individualmente. Si seleccionas "Administrador", todas las otras opciones
|
||||
serán activadas automáticamente. La administración de usuarios es un privilegio de administrador.
|
||||
profileSettings: Ajustes del perfil
|
||||
ruleExample1: >
|
||||
previene el acceso a una extensión de archivo (Como .git) en
|
||||
cada carpeta.
|
||||
ruleExample2: bloquea el acceso al archivo llamado Caddyfile en la carpeta raíz.
|
||||
rules: Reglas
|
||||
rulesHelp1: >
|
||||
Aquí puedes definir un conjunto de reglas de permisos para este usuario
|
||||
específico. Los archivos bloqueados no se mostrarán en las listas y no serán accesibles
|
||||
por el usuario. Puedes utilizar regex y rutas relativas a la raíz del usuario.
|
||||
rulesHelp2: >
|
||||
Cada regla va en una línea diferente, y debe comenzar con la palabra clave
|
||||
{0} or {1}. Entonces, debes escribir {2} si estás usando una expresión regular (REGEX) y
|
||||
luego la expresión o la ruta.
|
||||
scope: Raíz
|
||||
settingsUpdated: ¡Ajustes actualizados!
|
||||
user: Usuario
|
||||
userCommands: Comandos
|
||||
userCommandsHelp: >
|
||||
Una lista separada por espacios con los comandos permitidos para este usuario.
|
||||
Ejemplo:
|
||||
userCreated: ¡Usuario creado!
|
||||
userDeleted: ¡Usuario eliminado!
|
||||
userManagement: Administración de usuarios
|
||||
username: Usuario
|
||||
users: Usuarios
|
||||
userUpdated: ¡Usuario actualizado!
|
||||
sidebar:
|
||||
help: Ayuda
|
||||
logout: Cerrar sesión
|
||||
myFiles: Mis archivos
|
||||
newFile: Nuevo archivo
|
||||
newFolder: Nueva carpeta
|
||||
settings: Ajustes
|
||||
siteSettings: Ajustes del sitio
|
||||
hugoNew: Nuevo Hugo
|
||||
preview: Vista previa
|
||||
search:
|
||||
images: Images
|
||||
music: Música
|
||||
pdf: PDF
|
||||
pressToExecute: Presiona enter para ejecutar.
|
||||
pressToSearch: Presiona enter para buscar.
|
||||
search: Buscar...
|
||||
searchOrCommand: Buscar o ejecutar un comando...
|
||||
searchOrSupportedCommand: 'Buscar o ejecutar uno de los comandos soportados:'
|
||||
type: Escribe y presiona enter para buscar.
|
||||
types: Tipos
|
||||
video: Vídeo
|
||||
writeToSearch: Escribe aquí para buscar
|
||||
languages:
|
||||
en: English
|
||||
fr: Français
|
||||
pt: Português
|
||||
es: Español
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
zhTW: 中文 (繁體)
|
||||
|
||||
time:
|
||||
unit: Unidad
|
||||
seconds: Segundos
|
||||
minutes: Minutos
|
||||
hours: Horas
|
||||
days: Días
|
||||
194
assets/src/i18n/fr.yaml
Normal file
194
assets/src/i18n/fr.yaml
Normal file
@@ -0,0 +1,194 @@
|
||||
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: 中文 (繁體)
|
||||
es: Español
|
||||
time:
|
||||
unit: Unité de temps
|
||||
seconds: Secondes
|
||||
minutes: Minutes
|
||||
hours: Heures
|
||||
days: Jours
|
||||
@@ -1,22 +1,60 @@
|
||||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import en from './en.yaml'
|
||||
import fr from './fr.yaml'
|
||||
import pt from './pt.yaml'
|
||||
import ja from './ja.yaml'
|
||||
import zhCN from './zh-cn.yaml'
|
||||
import zhTW from './zh-tw.yaml'
|
||||
import es from './es.yaml'
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
export function detectLocale () {
|
||||
let locale = (navigator.language || navigator.browserLangugae).toLowerCase()
|
||||
switch (true) {
|
||||
case /^en.*/i.test(locale):
|
||||
locale = 'en'
|
||||
break
|
||||
case /^fr.*/i.test(locale):
|
||||
locale = 'fr'
|
||||
break
|
||||
case /^pt.*/i.test(locale):
|
||||
locale = 'pt'
|
||||
break
|
||||
case /^ja.*/i.test(locale):
|
||||
locale = 'ja'
|
||||
break
|
||||
case /^zh-CN/i.test(locale):
|
||||
locale = 'zh-cn'
|
||||
break
|
||||
case /^zh-TW/i.test(locale):
|
||||
locale = 'zh-tw'
|
||||
break
|
||||
case /^zh.*/i.test(locale):
|
||||
locale = 'zh-cn'
|
||||
break
|
||||
case /^es.*/i.test(locale):
|
||||
locale = 'es'
|
||||
break
|
||||
default:
|
||||
locale = 'en'
|
||||
}
|
||||
|
||||
return locale
|
||||
}
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: 'en',
|
||||
locale: detectLocale(),
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
'en': en,
|
||||
'fr': fr,
|
||||
'pt': pt,
|
||||
'ja': ja,
|
||||
'zh-cn': zhCN,
|
||||
'zh-tw': zhTW
|
||||
'zh-tw': zhTW,
|
||||
'es': es
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ buttons:
|
||||
update: 更新
|
||||
upload: アップロード
|
||||
permalink: 固定リンク
|
||||
success:
|
||||
linkCopied: リンクがコピーされました!
|
||||
errors:
|
||||
forbidden: アクセスが拒否されました。
|
||||
internal: 内部エラーが発生しました。
|
||||
@@ -116,12 +118,13 @@ settings:
|
||||
ここで、名前付きイベントに実行するコマンドを設定することができます。\
|
||||
一行にコマンド一つを入力してください。\
|
||||
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\
|
||||
環境変数 file はファイルのパスに割り当てられます。"
|
||||
環境変数 FILE はファイルのパスに割り当てられます。"
|
||||
commandsUpdated: コマンドは更新されました!
|
||||
customStylesheet: カスタムスタイルシ ート
|
||||
examples: 例
|
||||
globalSettings: グローバル設定
|
||||
language: 言語
|
||||
lockPassword: 新しいパスワードを変更に禁止
|
||||
newPassword: 新しいパスワード
|
||||
newPasswordConfirm: 新しいパスワードを確認します
|
||||
newUser: 新しいユーザー
|
||||
@@ -165,7 +168,6 @@ sidebar:
|
||||
myFiles: 私のファイル
|
||||
newFile: 新しいファイルを作成
|
||||
newFolder: 新しいフォルダを作成
|
||||
servedWith: サービス提供者
|
||||
settings: 設定
|
||||
siteSettings: サイト設定
|
||||
hugoNew: Hugo New
|
||||
@@ -185,10 +187,12 @@ search:
|
||||
writeToSearch: ここにキーワードを入力してください
|
||||
languages:
|
||||
en: English
|
||||
fr: Français
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
zhTW: 中文 (繁體)
|
||||
es: Español
|
||||
time:
|
||||
unit: 時間単位
|
||||
seconds: 秒
|
||||
|
||||
@@ -31,6 +31,8 @@ buttons:
|
||||
update: Atualizar
|
||||
upload: Enviar
|
||||
permalink: Obter link permanente
|
||||
success:
|
||||
linkCopied: Link copiado!
|
||||
errors:
|
||||
forbidden: Tu não és bem-vindo aqui.
|
||||
internal: Algo correu bastante mal.
|
||||
@@ -66,10 +68,12 @@ help:
|
||||
help: Ajuda
|
||||
languages:
|
||||
en: English
|
||||
fr: Français
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
zhTW: 中文 (繁體)
|
||||
es: Español
|
||||
login:
|
||||
password: Palavra-passe
|
||||
submit: Login
|
||||
@@ -136,12 +140,13 @@ settings:
|
||||
Pode definir um conjunto de comandos a executar em determiandos eventos.
|
||||
Deve escrever um comando por linha. Se o evento estiver relacionado com ficheiros,
|
||||
como antes e depois de guardar, irá existir uma variável de ambiente denominada
|
||||
"file" com o caminho do ficheiro.
|
||||
"FILE" com o caminho do ficheiro.
|
||||
commandsUpdated: Comandos atualizados!
|
||||
customStylesheet: Estilos Personalizados
|
||||
examples: Exemplos
|
||||
globalSettings: Configurações Globais
|
||||
language: Linguagem
|
||||
lockPassword: Não permitir que o utilizador altere a palavra-passe
|
||||
newPassword: Nova palavra-passe
|
||||
newPasswordConfirm: Confirme a nova palavra-passe
|
||||
newUser: Novo Utilizador
|
||||
@@ -189,7 +194,6 @@ sidebar:
|
||||
newFile: Novo ficheiro
|
||||
newFolder: Nova pasta
|
||||
preview: Pré-visualizar
|
||||
servedWith: Servido com
|
||||
settings: Configurações
|
||||
siteSettings: Configurações do Site
|
||||
time:
|
||||
|
||||
@@ -31,6 +31,8 @@ buttons:
|
||||
update: 更新
|
||||
upload: 上传
|
||||
permalink: 获取永久链接
|
||||
success:
|
||||
linkCopied: 链接已复制!
|
||||
errors:
|
||||
forbidden: 你被禁止访问。
|
||||
internal: 内部出现麻烦了。
|
||||
@@ -115,12 +117,13 @@ settings:
|
||||
commandsHelp: "\
|
||||
在这里,您可以设置在指定事件下执行的命令,一行一条。\
|
||||
若事件与文件相关,如“在保存文件前”,\
|
||||
则文件的路径会被赋值给环境变量 \"file\"。"
|
||||
则文件的路径会被赋值给环境变量 \"FILE\"。"
|
||||
commandsUpdated: 命令已更新!
|
||||
customStylesheet: 自定义样式表
|
||||
examples: 例子
|
||||
globalSettings: 全局设置
|
||||
language: 语言
|
||||
lockPassword: 禁止用户修改密码
|
||||
newPassword: 您的新密码
|
||||
newPasswordConfirm: 重输一遍新密码
|
||||
newUser: 新建用户
|
||||
@@ -163,7 +166,6 @@ sidebar:
|
||||
myFiles: 我的文件
|
||||
newFile: 新建文件
|
||||
newFolder: 新建文件夹
|
||||
servedWith: '服务提供者:'
|
||||
settings: 设置
|
||||
siteSettings: 网站设置
|
||||
hugoNew: Hugo New
|
||||
@@ -183,10 +185,12 @@ search:
|
||||
writeToSearch: 请输入要搜索的内容
|
||||
languages:
|
||||
en: English
|
||||
fr: Français
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
zhTW: 中文 (繁體)
|
||||
es: Español
|
||||
time:
|
||||
unit: 时间单位
|
||||
seconds: 秒
|
||||
|
||||
@@ -4,7 +4,7 @@ buttons:
|
||||
close: 關閉
|
||||
copy: 複製
|
||||
copyFile: 複製檔案
|
||||
copyToClipboard: 複製到剪貼板
|
||||
copyToClipboard: 複製到剪貼簿
|
||||
create: 建立
|
||||
delete: 刪除
|
||||
download: 下載
|
||||
@@ -31,8 +31,10 @@ buttons:
|
||||
update: 更新
|
||||
upload: 上傳
|
||||
permalink: 獲取永久連結
|
||||
success:
|
||||
linkCopied: 連結已複製!
|
||||
errors:
|
||||
forbidden: 你被禁止訪問。
|
||||
forbidden: 你被禁止存取。
|
||||
internal: 內部出現麻煩了。
|
||||
notFound: 找不到檔案。
|
||||
files:
|
||||
@@ -45,7 +47,7 @@ files:
|
||||
lastModified: 最後修改
|
||||
loading: 讀取中...
|
||||
lonely: 這裡沒有任何檔案...
|
||||
metadata: 中繼資料
|
||||
metadata: 詮釋資料
|
||||
multipleSelectionEnabled: 多選模式已開啟
|
||||
name: 名稱
|
||||
size: 大小
|
||||
@@ -100,70 +102,70 @@ prompts:
|
||||
show: 顯示
|
||||
size: 大小
|
||||
schedule: 計畫
|
||||
scheduleMessage: 請選擇發佈這篇帖子的日期。
|
||||
newArchetype: 建立一個基於原型的新帖子。您的檔案將會建立在內容資料夾中。
|
||||
scheduleMessage: 請選擇發佈這篇貼文的日期。
|
||||
newArchetype: 建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。
|
||||
settings:
|
||||
admin: 管理員
|
||||
administrator: 管理員
|
||||
allowCommands: 執行命令
|
||||
allowEdit: 編輯、重命名或刪除檔案/目錄
|
||||
allowNew: 創建新檔案和目錄
|
||||
allowPublish: 發佈新的帖子與頁面
|
||||
allowPublish: 發佈新的貼文與頁面
|
||||
avoidChanges: '(留空以避免更改)'
|
||||
changePassword: 更改密碼
|
||||
commands: 命令
|
||||
commandsHelp: "\
|
||||
在這裡,您可以設定在指定事件下執行的命令,一行一條。\
|
||||
若事件與檔案相關,如“在保存檔案前”,\
|
||||
則檔案的路徑會被賦值給環境變數 \"file\"。"
|
||||
則檔案的路徑會被賦值給環境變數 \"FILE\"。"
|
||||
commandsUpdated: 命令已更新!
|
||||
customStylesheet: 自定義樣式表
|
||||
examples: 例子
|
||||
examples: 範例
|
||||
globalSettings: 全域設定
|
||||
language: 語言
|
||||
lockPassword: 禁止使用者修改密碼
|
||||
newPassword: 您的新密碼
|
||||
newPasswordConfirm: 重輸一遍新密碼
|
||||
newUser: 建立用戶
|
||||
newUser: 建立使用者
|
||||
password: 密碼
|
||||
passwordUpdated: 密碼已更新!
|
||||
permissions: 權限
|
||||
permissionsHelp: "\
|
||||
您可以將該用戶設置為管理員,也可以單獨選擇各項權限。\
|
||||
您可以將該使用者設置為管理員,也可以單獨選擇各項權限。\
|
||||
如果選擇了“管理員”,則其他的選項會被自動勾上,\
|
||||
同時該用戶可以管理其他用戶。"
|
||||
同時該使用者可以管理其他使用者。"
|
||||
profileSettings: 設定檔設定
|
||||
ruleExample1: "\
|
||||
封鎖用戶訪問所有資料夾下任何以 . 開頭的檔案\
|
||||
封鎖使用者存取所有資料夾下任何以 . 開頭的檔案\
|
||||
(隱藏文件, 例如: .git, .gitignore)。"
|
||||
ruleExample2: 封鎖用戶訪問其目錄範圍的根目錄下名為 Caddyfile 的檔案。
|
||||
ruleExample2: 封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。
|
||||
rules: 規則
|
||||
rulesHelp1: "\
|
||||
您可以為該用戶製定一組黑名單或白名單式的規則,\
|
||||
被屏蔽的檔案將不會顯示在清單中,用戶也無權限訪問,\
|
||||
您可以為該使用者製定一組黑名單或白名單式的規則,\
|
||||
被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,\
|
||||
支持相對於目錄範圍的路徑。"
|
||||
rulesHelp2: "\
|
||||
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
|
||||
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
|
||||
scope: 目錄範圍
|
||||
settingsUpdated: 設定已更新!
|
||||
user: 用戶
|
||||
userCommands: 用戶命令
|
||||
user: 使用者
|
||||
userCommands: 使用者命令
|
||||
userCommandsHelp: "\
|
||||
指定該用戶可以執行的命令,用空格分隔。\
|
||||
指定該使用者可以執行的命令,用空格分隔。\
|
||||
例如:"
|
||||
userCreated: 用戶已建立!
|
||||
userDeleted: 用戶已刪除!
|
||||
userManagement: 用戶管理
|
||||
username: 用戶名
|
||||
users: 用戶
|
||||
userUpdated: 用戶已更新!
|
||||
userCreated: 使用者已建立!
|
||||
userDeleted: 使用者已刪除!
|
||||
userManagement: 使用者管理
|
||||
username: 使用者名稱
|
||||
users: 使用者
|
||||
userUpdated: 使用者已更新!
|
||||
sidebar:
|
||||
help: 幫助
|
||||
logout: 登出
|
||||
myFiles: 我的檔案
|
||||
newFile: 建立檔案
|
||||
newFolder: 建立資料夾
|
||||
servedWith: '服務提供者:'
|
||||
settings: 設定
|
||||
siteSettings: 網站設定
|
||||
hugoNew: Hugo New
|
||||
@@ -183,9 +185,12 @@ search:
|
||||
writeToSearch: 請輸入要搜尋的內容
|
||||
languages:
|
||||
en: English
|
||||
fr: Français
|
||||
pt: Português
|
||||
ja: 日本語
|
||||
zhCN: 中文 (简体)
|
||||
zhTW: 中文 (繁體)
|
||||
es: Español
|
||||
time:
|
||||
unit: 時間單位
|
||||
seconds: 秒
|
||||
|
||||
@@ -3,9 +3,46 @@ import App from './App'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
import Noty from 'noty'
|
||||
|
||||
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 */
|
||||
new Vue({
|
||||
el: '#app',
|
||||
|
||||
@@ -3,14 +3,15 @@ import Router from 'vue-router'
|
||||
import Login from '@/views/Login'
|
||||
import Layout from '@/views/Layout'
|
||||
import Files from '@/views/Files'
|
||||
import Users from '@/views/Users'
|
||||
import User from '@/views/User'
|
||||
import GlobalSettings from '@/views/GlobalSettings'
|
||||
import ProfileSettings from '@/views/ProfileSettings'
|
||||
import Users from '@/views/settings/Users'
|
||||
import User from '@/views/settings/User'
|
||||
import Settings from '@/views/Settings'
|
||||
import GlobalSettings from '@/views/settings/Global'
|
||||
import ProfileSettings from '@/views/settings/Profile'
|
||||
import Error403 from '@/views/errors/403'
|
||||
import Error404 from '@/views/errors/404'
|
||||
import Error500 from '@/views/errors/500'
|
||||
import auth from '@/utils/auth.js'
|
||||
import auth from '@/utils/auth'
|
||||
import store from '@/store'
|
||||
|
||||
Vue.use(Router)
|
||||
@@ -49,10 +50,14 @@ const router = new Router({
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: Settings,
|
||||
redirect: {
|
||||
path: '/settings/profile'
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
disableOnNoAuth: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/settings/profile',
|
||||
name: 'Profile Settings',
|
||||
@@ -66,6 +71,24 @@ const router = new Router({
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/settings/users',
|
||||
name: 'Users',
|
||||
component: Users,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/settings/users/*',
|
||||
name: 'User',
|
||||
component: User,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
@@ -81,22 +104,6 @@ const router = new Router({
|
||||
name: 'Internal Server Error',
|
||||
component: Error500
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: Users,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/users/*',
|
||||
name: 'User',
|
||||
component: User,
|
||||
meta: {
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/files',
|
||||
redirect: {
|
||||
@@ -123,17 +130,18 @@ router.beforeEach((to, from, next) => {
|
||||
auth.loggedIn()
|
||||
.then(() => {
|
||||
if (to.matched.some(record => record.meta.requiresAdmin)) {
|
||||
if (store.state.user.admin) {
|
||||
next()
|
||||
if (!store.state.user.admin) {
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next({
|
||||
path: '/403'
|
||||
})
|
||||
|
||||
if (to.matched.some(record => record.meta.disableOnNoAuth)) {
|
||||
if (store.state.noAuth) {
|
||||
next({ path: '/403' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
@@ -12,9 +12,18 @@ const state = {
|
||||
key: '',
|
||||
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'),
|
||||
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: '',
|
||||
progress: 0,
|
||||
schedule: '',
|
||||
loading: false,
|
||||
reload: false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import i18n from '@/i18n'
|
||||
import * as i18n from '@/i18n'
|
||||
import moment from 'moment'
|
||||
|
||||
const mutations = {
|
||||
@@ -27,10 +27,17 @@ const mutations = {
|
||||
setLoading: (state, value) => { state.loading = value },
|
||||
setReload: (state, value) => { state.reload = value },
|
||||
setUser: (state, value) => {
|
||||
moment.locale(value.locale)
|
||||
i18n.locale = value.locale
|
||||
let locale = value.locale
|
||||
|
||||
if (locale === '') {
|
||||
locale = i18n.detectLocale()
|
||||
}
|
||||
|
||||
moment.locale(locale)
|
||||
i18n.default.locale = locale
|
||||
state.user = value
|
||||
},
|
||||
setCSS: (state, value) => (state.css = value),
|
||||
setJWT: (state, value) => (state.jwt = value),
|
||||
multiple: (state, value) => (state.multiple = value),
|
||||
addSelected: (state, value) => (state.selected.push(value)),
|
||||
@@ -45,8 +52,12 @@ const mutations = {
|
||||
resetSelected: (state) => {
|
||||
state.selected = []
|
||||
},
|
||||
listingDisplay: (state, value) => {
|
||||
state.req.display = value
|
||||
updateUser: (state, value) => {
|
||||
if (typeof value !== 'object') return
|
||||
|
||||
for (let field in value) {
|
||||
state.user[field] = value[field]
|
||||
}
|
||||
},
|
||||
updateRequest: (state, value) => {
|
||||
state.req = value
|
||||
@@ -61,6 +72,9 @@ const mutations = {
|
||||
},
|
||||
setSchedule: (state, value) => {
|
||||
state.schedule = value
|
||||
},
|
||||
setProgress: (state, value) => {
|
||||
state.progress = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export function fetch (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
switch (request.status) {
|
||||
@@ -41,7 +41,7 @@ export function remove (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
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)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${store.state.baseURL}/api/resource${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
if (typeof onupload === 'function') {
|
||||
request.upload.onprogress = onupload
|
||||
}
|
||||
|
||||
if (overwrite) {
|
||||
request.setRequestHeader('Action', `override`)
|
||||
@@ -91,7 +95,7 @@ export function put (url, content = '', publish = false, date = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
request.setRequestHeader('Publish', publish)
|
||||
|
||||
if (date !== '') {
|
||||
@@ -121,7 +125,7 @@ function moveCopy (items, copy = false) {
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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)
|
||||
|
||||
if (copy) {
|
||||
@@ -158,7 +162,7 @@ export function checksum (url, algo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -222,7 +226,7 @@ export function getSettings () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/settings/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -251,7 +255,7 @@ export function updateSettings (param, which) {
|
||||
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
switch (request.status) {
|
||||
@@ -274,7 +278,7 @@ export function getUsers () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
switch (request.status) {
|
||||
@@ -295,7 +299,7 @@ export function getUser (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
switch (request.status) {
|
||||
@@ -316,7 +320,7 @@ export function newUser (user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
switch (request.status) {
|
||||
@@ -341,7 +345,7 @@ export function updateUser (user, which) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
@@ -366,7 +370,7 @@ export function deleteUser (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
switch (request.status) {
|
||||
@@ -391,7 +395,7 @@ export function getShare (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -410,7 +414,7 @@ export function deleteShare (hash) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -435,7 +439,7 @@ export function share (url, expires = '', unit = 'hours') {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
if (request.status === 200) {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import cookie from './cookie'
|
||||
import store from '@/store'
|
||||
import router from '@/router'
|
||||
import { Base64 } from 'js-base64'
|
||||
|
||||
function parseToken (token) {
|
||||
let path = store.state.baseURL
|
||||
if (path === '') path = '/'
|
||||
document.cookie = `auth=${token}; max-age=86400; path=${path}`
|
||||
let res = token.split('.')
|
||||
let user = JSON.parse(window.atob(res[1]))
|
||||
let user = JSON.parse(Base64.decode(res[1]))
|
||||
if (!user.commands) {
|
||||
user.commands = []
|
||||
}
|
||||
|
||||
store.commit('setJWT', token)
|
||||
store.commit('setUser', user)
|
||||
}
|
||||
@@ -16,7 +21,7 @@ function loggedIn () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
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 = () => {
|
||||
if (request.status === 200) {
|
||||
@@ -31,8 +36,8 @@ function loggedIn () {
|
||||
})
|
||||
}
|
||||
|
||||
function login (user, password) {
|
||||
let data = {username: user, password: password}
|
||||
function login (user, password, captcha) {
|
||||
let data = {username: user, password: password, recaptcha: captcha}
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${store.state.baseURL}/api/auth/get`, true)
|
||||
|
||||
@@ -210,7 +210,7 @@ export default {
|
||||
}
|
||||
},
|
||||
scroll (event) {
|
||||
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
|
||||
if (this.req.kind !== 'listing' || this.$store.state.user.viewMode === 'mosaic') return
|
||||
|
||||
let top = 112 - window.scrollY
|
||||
|
||||
|
||||
@@ -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>
|
||||
<div>
|
||||
<div id="progress">
|
||||
<div v-bind:style="{ width: $store.state.progress + '%' }"></div>
|
||||
</div>
|
||||
<site-header></site-header>
|
||||
<sidebar></sidebar>
|
||||
<main>
|
||||
<router-view v-on:css-updated="updateCSS"></router-view>
|
||||
<router-view @css="$emit('update:css')"></router-view>
|
||||
</main>
|
||||
<prompts></prompts>
|
||||
</div>
|
||||
@@ -31,23 +34,10 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateCSS()
|
||||
this.$emit('update:css')
|
||||
},
|
||||
methods: {
|
||||
updateCSS () {
|
||||
let css = this.$store.state.user.css
|
||||
|
||||
let style = document.querySelector('style[title="user-css"]')
|
||||
if (style !== undefined && style !== null) {
|
||||
style.parentElement.removeChild(style)
|
||||
}
|
||||
|
||||
style = document.createElement('style')
|
||||
style.title = 'user-css'
|
||||
style.type = 'text/css'
|
||||
style.appendChild(document.createTextNode(css))
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
beforeDestroy () {
|
||||
this.$emit('clean:css')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div id="login">
|
||||
<div id="login" :class="{ recaptcha: recaptcha.length > 0 }">
|
||||
<form @submit="submit">
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<h1>File Manager</h1>
|
||||
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
|
||||
<input type="text" v-model="username" :placeholder="$t('login.username')">
|
||||
<input type="password" v-model="password" :placeholder="$t('login.password')">
|
||||
<div v-if="recaptcha.length" id="recaptcha"></div>
|
||||
<input type="submit" :value="$t('login.submit')">
|
||||
</form>
|
||||
</div>
|
||||
@@ -13,9 +14,12 @@
|
||||
|
||||
<script>
|
||||
import auth from '@/utils/auth'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
props: ['dependencies'],
|
||||
computed: mapState(['recaptcha']),
|
||||
data: function () {
|
||||
return {
|
||||
wrong: false,
|
||||
@@ -23,8 +27,23 @@ export default {
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.dependencies) this.setup()
|
||||
},
|
||||
watch: {
|
||||
dependencies: function (val) {
|
||||
if (val) this.setup()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
setup () {
|
||||
if (this.recaptcha.length === 0) return
|
||||
|
||||
window.grecaptcha.render('recaptcha', {
|
||||
sitekey: this.recaptcha
|
||||
})
|
||||
},
|
||||
submit (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -33,7 +52,17 @@ export default {
|
||||
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 }) })
|
||||
.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,296 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="save" class="dashboard">
|
||||
<ul id="nav">
|
||||
<li>
|
||||
<router-link to="/users">
|
||||
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.userManagement') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li></li>
|
||||
</ul>
|
||||
|
||||
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
|
||||
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
|
||||
|
||||
<p><label for="username">{{ $t('settings.username') }}</label><input type="text" v-model="username" id="username"></p>
|
||||
<p><label for="password">{{ $t('settings.password') }}</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
|
||||
<p><label for="scope">{{ $t('settings.scope') }}</label><input type="text" v-model="filesystem" id="scope"></p>
|
||||
<p>
|
||||
<label for="locale">{{ $t('settings.language') }}</label>
|
||||
<languages id="locale" :selected.sync="locale"></languages>
|
||||
</p>
|
||||
|
||||
<h2>{{ $t('settings.permissions') }}</h2>
|
||||
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
|
||||
|
||||
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
|
||||
<p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
|
||||
|
||||
<h3>{{ $t('settings.userCommands') }}</h3>
|
||||
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
|
||||
<input type="text" v-model.trim="commands">
|
||||
|
||||
<h2>{{ $t('settings.rules') }}</h2>
|
||||
|
||||
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
|
||||
|
||||
<i18n path="settings.rulesHelp2" tag="p" class="small">
|
||||
<code>allow</code><code>disallow</code><code>regex</code>
|
||||
</i18n>
|
||||
|
||||
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
|
||||
|
||||
<ul class="small">
|
||||
<li><code>disallow regex \\/\\..+</code> - {{ $t('settings.ruleExample1') }}</li>
|
||||
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
|
||||
</ul>
|
||||
|
||||
<textarea v-model.trim="rules"></textarea>
|
||||
|
||||
<h2>{{ $t('settings.customStylesheet') }}</h2>
|
||||
|
||||
<textarea name="css"></textarea>
|
||||
|
||||
<p>
|
||||
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<input type="submit" :value="$t('buttons.save')">
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<div v-if="$store.state.show === 'deleteUser'" class="prompt">
|
||||
<h3>Delete User</h3>
|
||||
<p>Are you sure you want to delete this user?</p>
|
||||
<div>
|
||||
<button @click="deleteUser" autofocus>{{ $t('buttons.delete') }}</button>
|
||||
<button class="cancel"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">
|
||||
{{ $t('buttons.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
|
||||
import Languages from '@/components/Languages'
|
||||
|
||||
export default {
|
||||
name: 'user',
|
||||
components: { Languages },
|
||||
data: () => {
|
||||
return {
|
||||
id: 0,
|
||||
admin: false,
|
||||
allowNew: false,
|
||||
allowEdit: false,
|
||||
allowCommands: false,
|
||||
allowPublish: false,
|
||||
permissions: {},
|
||||
password: '',
|
||||
username: '',
|
||||
filesystem: '',
|
||||
rules: '',
|
||||
locale: '',
|
||||
css: '',
|
||||
commands: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
passwordPlaceholder () {
|
||||
if (this.$route.path === '/users/new') return ''
|
||||
return this.$t('settings.avoidChanges')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
watch: {
|
||||
'$route': 'fetchData',
|
||||
admin: function () {
|
||||
if (!this.admin) return
|
||||
this.allowCommands = true
|
||||
this.allowEdit = true
|
||||
this.allowNew = true
|
||||
this.allowPublish = true
|
||||
for (let key in this.permissions) {
|
||||
this.permissions[key] = true
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['closeHovers']),
|
||||
fetchData () {
|
||||
let user = this.$route.params[0]
|
||||
|
||||
if (this.$route.path === '/users/new') {
|
||||
user = 'base'
|
||||
}
|
||||
|
||||
getUser(user).then(user => {
|
||||
this.id = user.ID
|
||||
this.admin = user.admin
|
||||
this.allowCommands = user.allowCommands
|
||||
this.allowNew = user.allowNew
|
||||
this.allowEdit = user.allowEdit
|
||||
this.allowPublish = user.allowPublish
|
||||
this.filesystem = user.filesystem
|
||||
this.username = user.username
|
||||
this.commands = user.commands.join(' ')
|
||||
this.css = user.css
|
||||
this.permissions = user.permissions
|
||||
this.locale = user.locale
|
||||
|
||||
for (let rule of user.rules) {
|
||||
if (rule.allow) {
|
||||
this.rules += 'allow '
|
||||
} else {
|
||||
this.rules += 'disallow '
|
||||
}
|
||||
|
||||
if (rule.regex) {
|
||||
this.rules += 'regex ' + rule.regexp.raw
|
||||
} else {
|
||||
this.rules += rule.path
|
||||
}
|
||||
|
||||
this.rules += '\n'
|
||||
}
|
||||
|
||||
this.rules = this.rules.trim()
|
||||
}).catch(() => {
|
||||
this.$router.push({ path: '/users/new' })
|
||||
})
|
||||
},
|
||||
capitalize (name) {
|
||||
let splitted = name.split(/(?=[A-Z])/)
|
||||
name = ''
|
||||
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
||||
}
|
||||
|
||||
return name.slice(0, -1)
|
||||
},
|
||||
reset () {
|
||||
this.id = 0
|
||||
this.admin = false
|
||||
this.allowNew = false
|
||||
this.allowEdit = false
|
||||
this.allowPublish = false
|
||||
this.permissins = {}
|
||||
this.allowCommands = false
|
||||
this.password = ''
|
||||
this.username = ''
|
||||
this.filesystem = ''
|
||||
this.rules = ''
|
||||
this.locale = ''
|
||||
this.css = ''
|
||||
this.commands = ''
|
||||
},
|
||||
deletePrompt (event) {
|
||||
this.$store.commit('showHover', 'deleteUser')
|
||||
},
|
||||
deleteUser (event) {
|
||||
event.preventDefault()
|
||||
|
||||
deleteUser(this.id).then(location => {
|
||||
this.$router.push({ path: '/users' })
|
||||
this.$store.commit('showSuccess', this.$t('settings.userDeleted'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
},
|
||||
save (event) {
|
||||
event.preventDefault()
|
||||
let user = this.parseForm()
|
||||
|
||||
if (this.$route.path === '/users/new') {
|
||||
newUser(user).then(location => {
|
||||
this.$router.push({ path: location })
|
||||
this.$store.commit('showSuccess', this.$t('settings.userCreated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
updateUser(user).then(location => {
|
||||
if (user.ID === this.$store.state.user.ID) {
|
||||
this.$store.commit('setUser', user)
|
||||
}
|
||||
|
||||
this.$store.commit('showSuccess', this.$t('settings.userUpdated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
},
|
||||
parseForm () {
|
||||
let user = {
|
||||
ID: this.id,
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
filesystem: this.filesystem,
|
||||
admin: this.admin,
|
||||
allowCommands: this.allowCommands,
|
||||
allowNew: this.allowNew,
|
||||
allowEdit: this.allowEdit,
|
||||
allowPublish: this.allowPublish,
|
||||
permissions: this.permissions,
|
||||
css: this.css,
|
||||
locale: this.locale,
|
||||
commands: this.commands.split(' '),
|
||||
rules: []
|
||||
}
|
||||
|
||||
let rules = this.rules.split('\n')
|
||||
|
||||
for (let rawRule of rules) {
|
||||
let rule = {
|
||||
allow: true,
|
||||
path: '',
|
||||
regex: false,
|
||||
regexp: {
|
||||
raw: ''
|
||||
}
|
||||
}
|
||||
|
||||
rawRule = rawRule.split(' ')
|
||||
|
||||
// Skip a malformed rule
|
||||
if (rawRule.length < 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip a malformed rule
|
||||
if (rawRule[0] !== 'allow' && rawRule[0] !== 'disallow') {
|
||||
continue
|
||||
}
|
||||
|
||||
rule.allow = (rawRule[0] === 'allow')
|
||||
rawRule.shift()
|
||||
|
||||
if (rawRule[0] === 'regex') {
|
||||
rule.regex = true
|
||||
rawRule.shift()
|
||||
rule.regexp.raw = rawRule.join(' ')
|
||||
} else {
|
||||
rule.path = rawRule.join(' ')
|
||||
}
|
||||
|
||||
user.rules.push(rule)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -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>
|
||||
319
assets/src/views/settings/User.vue
Normal file
319
assets/src/views/settings/User.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="save" class="card">
|
||||
<div class="card-title">
|
||||
<h2 v-if="id === 0">{{ $t('settings.newUser') }}</h2>
|
||||
<h2 v-else>{{ $t('settings.user') }} {{ username }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>
|
||||
<label for="username">{{ $t('settings.username') }}</label>
|
||||
<input type="text" v-model="username" id="username">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="password">{{ $t('settings.password') }}</label>
|
||||
<input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="scope">{{ $t('settings.scope') }}</label>
|
||||
<input type="text" v-model="filesystem" id="scope">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="locale">{{ $t('settings.language') }}</label>
|
||||
<languages id="locale" :selected.sync="locale"></languages>
|
||||
</p>
|
||||
|
||||
<p><input type="checkbox" :disabled="admin" v-model="lockPassword"> {{ $t('settings.lockPassword') }}</p>
|
||||
|
||||
<h3>{{ $t('settings.permissions') }}</h3>
|
||||
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
|
||||
|
||||
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> {{ $t('settings.allowNew') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> {{ $t('settings.allowEdit') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> {{ $t('settings.allowCommands') }}</p>
|
||||
<p v-show="$store.state.staticGen.length"><input type="checkbox" :disabled="admin" v-model="allowPublish"> {{ $t('settings.allowPublish') }}</p>
|
||||
|
||||
<h3>{{ $t('settings.userCommands') }}</h3>
|
||||
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
|
||||
<input type="text" v-model.trim="commands">
|
||||
|
||||
<h3>{{ $t('settings.rules') }}</h3>
|
||||
|
||||
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
|
||||
|
||||
<i18n path="settings.rulesHelp2" tag="p" class="small">
|
||||
<code>allow</code><code>disallow</code><code>regex</code>
|
||||
</i18n>
|
||||
|
||||
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
|
||||
|
||||
<ul class="small">
|
||||
<li><code>disallow regex [\\\/]\..+</code> - {{ $t('settings.ruleExample1') }}</li>
|
||||
<li><code>disallow /Caddyfile</code> - {{ $t('settings.ruleExample2') }}</li>
|
||||
</ul>
|
||||
|
||||
<textarea v-model.trim="rules"></textarea>
|
||||
|
||||
<h3>{{ $t('settings.customStylesheet') }}</h3>
|
||||
|
||||
<textarea name="css"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="flat delete" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
|
||||
<input class="flat" type="submit" :value="$t('buttons.save')">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="$store.state.show === 'deleteUser'" class="card floating">
|
||||
<div class="card-content">
|
||||
<p>Are you sure you want to delete this user?</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button class="cancel flat"
|
||||
@click="closeHovers"
|
||||
autofocus
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">
|
||||
{{ $t('buttons.cancel') }}
|
||||
</button>
|
||||
<button class="flat"
|
||||
@click="deleteUser">
|
||||
{{ $t('buttons.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
|
||||
import Languages from '@/components/Languages'
|
||||
|
||||
export default {
|
||||
name: 'user',
|
||||
components: { Languages },
|
||||
data: () => {
|
||||
return {
|
||||
originalUser: null,
|
||||
id: 0,
|
||||
admin: false,
|
||||
allowNew: false,
|
||||
allowEdit: false,
|
||||
allowCommands: false,
|
||||
allowPublish: false,
|
||||
lockPassword: false,
|
||||
permissions: {},
|
||||
password: '',
|
||||
username: '',
|
||||
filesystem: '',
|
||||
rules: '',
|
||||
locale: '',
|
||||
css: '',
|
||||
commands: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
passwordPlaceholder () {
|
||||
if (this.$route.path === '/settings/users/new') return ''
|
||||
return this.$t('settings.avoidChanges')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
watch: {
|
||||
'$route': 'fetchData',
|
||||
admin: function () {
|
||||
if (!this.admin) return
|
||||
this.allowCommands = true
|
||||
this.allowEdit = true
|
||||
this.allowNew = true
|
||||
this.allowPublish = true
|
||||
this.lockPassword = false
|
||||
for (let key in this.permissions) {
|
||||
this.permissions[key] = true
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['closeHovers']),
|
||||
fetchData () {
|
||||
let user = this.$route.params[0]
|
||||
|
||||
if (this.$route.path === '/settings/users/new') {
|
||||
user = 'base'
|
||||
}
|
||||
|
||||
getUser(user).then(user => {
|
||||
this.originalUser = user
|
||||
this.id = user.ID
|
||||
this.admin = user.admin
|
||||
this.allowCommands = user.allowCommands
|
||||
this.allowNew = user.allowNew
|
||||
this.allowEdit = user.allowEdit
|
||||
this.allowPublish = user.allowPublish
|
||||
this.lockPassword = user.lockPassword
|
||||
this.filesystem = user.filesystem
|
||||
this.username = user.username
|
||||
this.css = user.css
|
||||
this.permissions = user.permissions
|
||||
this.locale = user.locale
|
||||
|
||||
if (user.commands) {
|
||||
this.commands = user.commands.join(' ')
|
||||
}
|
||||
|
||||
for (let rule of user.rules) {
|
||||
if (rule.allow) {
|
||||
this.rules += 'allow '
|
||||
} else {
|
||||
this.rules += 'disallow '
|
||||
}
|
||||
|
||||
if (rule.regex) {
|
||||
this.rules += 'regex ' + rule.regexp.raw
|
||||
} else {
|
||||
this.rules += rule.path
|
||||
}
|
||||
|
||||
this.rules += '\n'
|
||||
}
|
||||
|
||||
this.rules = this.rules.trim()
|
||||
}).catch(() => {
|
||||
this.$router.push({ path: '/settings/users/new' })
|
||||
})
|
||||
},
|
||||
capitalize (name) {
|
||||
let splitted = name.split(/(?=[A-Z])/)
|
||||
name = ''
|
||||
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + ' '
|
||||
}
|
||||
|
||||
return name.slice(0, -1)
|
||||
},
|
||||
reset () {
|
||||
this.id = 0
|
||||
this.admin = false
|
||||
this.allowNew = false
|
||||
this.allowEdit = false
|
||||
this.allowPublish = false
|
||||
this.permissins = {}
|
||||
this.allowCommands = false
|
||||
this.lockPassword = false
|
||||
this.password = ''
|
||||
this.username = ''
|
||||
this.filesystem = ''
|
||||
this.rules = ''
|
||||
this.locale = ''
|
||||
this.css = ''
|
||||
this.commands = ''
|
||||
},
|
||||
deletePrompt (event) {
|
||||
this.$store.commit('showHover', 'deleteUser')
|
||||
},
|
||||
deleteUser (event) {
|
||||
event.preventDefault()
|
||||
|
||||
deleteUser(this.id).then(location => {
|
||||
this.$router.push({ path: '/settings/users' })
|
||||
this.$showSuccess(this.$t('settings.userDeleted'))
|
||||
}).catch(e => {
|
||||
this.$showError(e)
|
||||
})
|
||||
},
|
||||
save (event) {
|
||||
event.preventDefault()
|
||||
let user = this.parseForm()
|
||||
|
||||
if (this.$route.path === '/settings/users/new') {
|
||||
newUser(user).then(location => {
|
||||
this.$router.push({ path: location })
|
||||
this.$showSuccess(this.$t('settings.userCreated'))
|
||||
}).catch(e => {
|
||||
this.$showError(e)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
updateUser(user).then(location => {
|
||||
if (user.ID === this.$store.state.user.ID) {
|
||||
this.$store.commit('setUser', user)
|
||||
}
|
||||
|
||||
this.$showSuccess(this.$t('settings.userUpdated'))
|
||||
}).catch(e => {
|
||||
this.$showError(e)
|
||||
})
|
||||
},
|
||||
parseForm () {
|
||||
let user = this.originalUser
|
||||
user.username = this.username
|
||||
user.password = this.password
|
||||
user.lockPassword = this.lockPassword
|
||||
user.filesystem = this.filesystem
|
||||
user.admin = this.admin
|
||||
user.allowCommands = this.allowCommands
|
||||
user.allowNew = this.allowNew
|
||||
user.allowEdit = this.allowEdit
|
||||
user.allowPublish = this.allowPublish
|
||||
user.permissions = this.permissions
|
||||
user.css = this.css
|
||||
user.locale = this.locale
|
||||
user.commands = this.commands.split(' ')
|
||||
user.rules = []
|
||||
|
||||
let rules = this.rules.split('\n')
|
||||
|
||||
for (let rawRule of rules) {
|
||||
let rule = {
|
||||
allow: true,
|
||||
path: '',
|
||||
regex: false,
|
||||
regexp: {
|
||||
raw: ''
|
||||
}
|
||||
}
|
||||
|
||||
rawRule = rawRule.split(' ')
|
||||
|
||||
// Skip a malformed rule
|
||||
if (rawRule.length < 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip a malformed rule
|
||||
if (rawRule[0] !== 'allow' && rawRule[0] !== 'disallow') {
|
||||
continue
|
||||
}
|
||||
|
||||
rule.allow = (rawRule[0] === 'allow')
|
||||
rawRule.shift()
|
||||
|
||||
if (rawRule[0] === 'regex') {
|
||||
rule.regex = true
|
||||
rawRule.shift()
|
||||
rule.regexp.raw = rawRule.join(' ')
|
||||
} else {
|
||||
rule.path = rawRule.join(' ')
|
||||
}
|
||||
|
||||
user.rules.push(rule)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
48
assets/src/views/settings/Users.vue
Normal file
48
assets/src/views/settings/Users.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.users') }}</h2>
|
||||
<router-link to="/settings/users/new"><button class="flat">{{ $t('buttons.new') }}</button></router-link>
|
||||
</div>
|
||||
|
||||
<div class="card-content full">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t('settings.username') }}</th>
|
||||
<th>{{ $t('settings.admin') }}</th>
|
||||
<th>{{ $t('settings.scope') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.username }}</td>
|
||||
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
|
||||
<td>{{ user.filesystem }}</td>
|
||||
<td class="small">
|
||||
<router-link :to="'/settings/users/' + user.ID"><i class="material-icons">mode_edit</i></router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'users',
|
||||
data: function () {
|
||||
return {
|
||||
users: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
api.getUsers().then(users => {
|
||||
this.users = users
|
||||
}).catch(error => {
|
||||
this.$showError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
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})
|
||||
}
|
||||
1
build.sh
1
build.sh
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Install rice tool if not present
|
||||
if ! [ -x "$(command -v rice)" ]; then
|
||||
|
||||
@@ -4,17 +4,11 @@
|
||||
package filemanager
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
. "github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/filemanager/caddy/parser"
|
||||
h "github.com/hacdias/filemanager/http"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
@@ -28,23 +22,18 @@ func init() {
|
||||
|
||||
type plugin struct {
|
||||
Next httpserver.Handler
|
||||
Configs []*config
|
||||
}
|
||||
|
||||
type config struct {
|
||||
*FileManager
|
||||
baseURL string
|
||||
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) {
|
||||
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
f.Configs[i].ServeHTTP(w, r)
|
||||
h.Handler(f.Configs[i]).ServeHTTP(w, r)
|
||||
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.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := parse(c)
|
||||
configs, err := parser.Parse(c, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -64,107 +53,3 @@ func setup(c *caddy.Controller) error {
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/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() {
|
||||
caddy.RegisterPlugin("hugo", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
@@ -175,3 +21,32 @@ type plugin struct {
|
||||
Next httpserver.Handler
|
||||
Configs []*filemanager.FileManager
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for i := range f.Configs {
|
||||
// Checks if this Path should be handled by File Manager.
|
||||
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
h.Handler(f.Configs[i]).ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// setup configures a new FileManager middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := parser.Parse(c, "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
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// setup configures a new FileManager middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := parse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return plugin{Configs: configs, Next: next}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
||||
var (
|
||||
configs []*filemanager.FileManager
|
||||
)
|
||||
|
||||
for c.Next() {
|
||||
// jekyll [directory] [admin] {
|
||||
// database path
|
||||
// }
|
||||
directory := "."
|
||||
admin := "/admin"
|
||||
database := ""
|
||||
noAuth := false
|
||||
|
||||
// Get the baseURL and baseScope
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) >= 1 {
|
||||
directory = args[0]
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
admin = args[1]
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "database":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
database = c.Val()
|
||||
case "no_auth":
|
||||
if !c.NextArg() {
|
||||
noAuth = true
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
noAuth, err = strconv.ParseBool(c.Val())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
caddyConf := httpserver.GetConfig(c)
|
||||
|
||||
path := filepath.Join(caddy.AssetsPath(), "jekyll")
|
||||
err := os.MkdirAll(path, 0700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if there is a database path and it is not absolute,
|
||||
// it will be relative to ".caddy" folder.
|
||||
if !filepath.IsAbs(database) && database != "" {
|
||||
database = filepath.Join(path, database)
|
||||
}
|
||||
|
||||
// If there is no database path on the settings,
|
||||
// store one in .caddy/jekyll/{name}.db.
|
||||
if database == "" {
|
||||
// The name of the database is the hashed value of a string composed
|
||||
// by the host, address path and the baseurl of this File Manager
|
||||
// instance.
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + admin))
|
||||
sha := hex.EncodeToString(hasher.Sum(nil))
|
||||
database = filepath.Join(path, sha+".db")
|
||||
|
||||
fmt.Println("[WARNING] A database is going to be created for your Jekyll instace at " + database +
|
||||
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
||||
}
|
||||
|
||||
m, err := filemanager.New(database, filemanager.User{
|
||||
Locale: "en",
|
||||
AllowCommands: true,
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
AllowPublish: true,
|
||||
Commands: []string{"git", "svn", "hg"},
|
||||
Rules: []*filemanager.Rule{{
|
||||
Regex: true,
|
||||
Allow: false,
|
||||
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
||||
}},
|
||||
CSS: "",
|
||||
FileSystem: fileutils.Dir(directory),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize the default settings for Jekyll.
|
||||
jekyll := &filemanager.Jekyll{
|
||||
Root: directory,
|
||||
Public: filepath.Join(directory, "_site"),
|
||||
Args: []string{},
|
||||
CleanPublic: true,
|
||||
}
|
||||
|
||||
// Attaches Hugo plugin to this file manager instance.
|
||||
err = m.EnableStaticGen(jekyll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.NoAuth = noAuth
|
||||
m.SetBaseURL(admin)
|
||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
||||
configs = append(configs, m)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
func (p plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for i := range p.Configs {
|
||||
// Checks if this Path should be handled by File Manager.
|
||||
if !httpserver.Path(r.URL.Path).Matches(p.Configs[i].BaseURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
p.Configs[i].ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return p.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("jekyll", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
@@ -175,3 +21,32 @@ type plugin struct {
|
||||
Next httpserver.Handler
|
||||
Configs []*filemanager.FileManager
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for i := range f.Configs {
|
||||
// Checks if this Path should be handled by File Manager.
|
||||
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
h.Handler(f.Configs[i]).ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// setup configures a new FileManager middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := parser.Parse(c, "jekyll")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return plugin{Configs: configs, Next: next}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
295
caddy/parser/parser.go
Normal file
295
caddy/parser/parser.go
Normal file
@@ -0,0 +1,295 @@
|
||||
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: "",
|
||||
ViewMode: "mosaic",
|
||||
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 != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode {
|
||||
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 instance 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,9 +10,14 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
"github.com/asdine/storm"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/filemanager/bolt"
|
||||
h "github.com/hacdias/filemanager/http"
|
||||
"github.com/hacdias/filemanager/staticgen"
|
||||
"github.com/hacdias/fileutils"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
@@ -25,8 +30,13 @@ var (
|
||||
scope string
|
||||
commands string
|
||||
logfile string
|
||||
staticgen string
|
||||
staticg string
|
||||
locale string
|
||||
baseurl string
|
||||
prefixurl string
|
||||
viewMode string
|
||||
recaptchakey string
|
||||
recaptchasecret string
|
||||
port int
|
||||
noAuth bool
|
||||
allowCommands bool
|
||||
@@ -34,7 +44,6 @@ var (
|
||||
allowNew bool
|
||||
allowPublish bool
|
||||
showVer bool
|
||||
version = "master"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -44,14 +53,19 @@ func init() {
|
||||
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(&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(&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(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
|
||||
flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users")
|
||||
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users")
|
||||
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
|
||||
flag.StringVar(&locale, "locale", "en", "Default locale for new users")
|
||||
flag.StringVar(&staticgen, "staticgen", "", "Static Generator you want to enable")
|
||||
flag.StringVar(&locale, "locale", "", "Default locale for new users, set it empty to enable auto detect from browser")
|
||||
flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable")
|
||||
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
|
||||
}
|
||||
|
||||
@@ -67,8 +81,13 @@ func setupViper() {
|
||||
viper.SetDefault("AllowNew", true)
|
||||
viper.SetDefault("AllowPublish", true)
|
||||
viper.SetDefault("StaticGen", "")
|
||||
viper.SetDefault("Locale", "en")
|
||||
viper.SetDefault("Locale", "")
|
||||
viper.SetDefault("NoAuth", false)
|
||||
viper.SetDefault("BaseURL", "")
|
||||
viper.SetDefault("PrefixURL", "")
|
||||
viper.SetDefault("ViewMode", filemanager.MosaicViewMode)
|
||||
viper.SetDefault("ReCaptchaKey", "")
|
||||
viper.SetDefault("ReCaptchaSecret", "")
|
||||
|
||||
viper.BindPFlag("Port", flag.Lookup("port"))
|
||||
viper.BindPFlag("Address", flag.Lookup("address"))
|
||||
@@ -83,21 +102,18 @@ func setupViper() {
|
||||
viper.BindPFlag("Locale", flag.Lookup("locale"))
|
||||
viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
|
||||
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
|
||||
viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
|
||||
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
|
||||
viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
|
||||
viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key"))
|
||||
viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret"))
|
||||
|
||||
viper.SetConfigName("filemanager")
|
||||
viper.AddConfigPath(".")
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
version = strings.TrimSpace(version)
|
||||
|
||||
if version == "" {
|
||||
fmt.Println("filemanager is at an untracked version")
|
||||
} else {
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
fmt.Println("filemanager version", version)
|
||||
}
|
||||
|
||||
fmt.Println("filemanager version", filemanager.Version)
|
||||
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.
|
||||
laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
|
||||
listener, err := net.Listen("tcp", laddr)
|
||||
@@ -205,7 +175,75 @@ func main() {
|
||||
fmt.Println("Listening on", listener.Addr().String())
|
||||
|
||||
// Starts the server.
|
||||
if err := http.Serve(listener, fm); err != nil {
|
||||
if err := http.Serve(listener, handler()); err != nil {
|
||||
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
|
||||
137
file.go
137
file.go
@@ -7,7 +7,6 @@ import (
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -23,13 +22,9 @@ import (
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidOption = errors.New("Invalid option")
|
||||
)
|
||||
|
||||
// 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).
|
||||
// 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"`
|
||||
// The name of the file.
|
||||
Name string `json:"name"`
|
||||
@@ -54,37 +49,35 @@ type file struct {
|
||||
// Stores the content of a text file.
|
||||
Content string `json:"content,omitempty"`
|
||||
|
||||
*listing `json:",omitempty"`
|
||||
*Listing `json:",omitempty"`
|
||||
|
||||
Metadata string `json:"metadata,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
// A listing is the context used to fill out a template.
|
||||
type listing struct {
|
||||
// A Listing is the context used to fill out a template.
|
||||
type Listing struct {
|
||||
// The items (files and folders) in the path.
|
||||
Items []*file `json:"items"`
|
||||
// The number of directories in the listing.
|
||||
Items []*File `json:"items"`
|
||||
// The number of directories in the Listing.
|
||||
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"`
|
||||
// Which sorting order is used.
|
||||
Sort string `json:"sort"`
|
||||
// And which 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
|
||||
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
|
||||
|
||||
i := &file{
|
||||
i := &File{
|
||||
URL: "/files" + url.String(),
|
||||
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)
|
||||
@@ -106,11 +99,11 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// getListing gets the information about a specific directory and its files.
|
||||
func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||
// GetListing gets the information about a specific directory and its files.
|
||||
func (i *File) GetListing(u *User, r *http.Request) error {
|
||||
// Gets the directory information using the Virtual File System of
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
@@ -123,7 +116,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||
}
|
||||
|
||||
var (
|
||||
fileinfos []*file
|
||||
fileinfos []*File
|
||||
dirCount, fileCount int
|
||||
)
|
||||
|
||||
@@ -134,12 +127,22 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
allowed := c.User.Allowed("/" + name)
|
||||
allowed := u.Allowed("/" + name)
|
||||
|
||||
if !allowed {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(f.Mode().String(), "L") {
|
||||
// It's a symbolic link
|
||||
// The FileInfo from Readdir treats symbolic link as a file only.
|
||||
info, err := os.Stat(f.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f = info
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
name += "/"
|
||||
dirCount++
|
||||
@@ -150,7 +153,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||
// Absolute URL
|
||||
url := url.URL{Path: baseurl + name}
|
||||
|
||||
i := &file{
|
||||
i := &File{
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
ModTime: f.ModTime(),
|
||||
@@ -166,7 +169,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||
fileinfos = append(fileinfos, i)
|
||||
}
|
||||
|
||||
i.listing = &listing{
|
||||
i.Listing = &Listing{
|
||||
Items: fileinfos,
|
||||
NumDirs: dirCount,
|
||||
NumFiles: fileCount,
|
||||
@@ -175,8 +178,8 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEditor gets the editor based on a Info struct
|
||||
func (i *file) getEditor() error {
|
||||
// GetEditor gets the editor based on a Info struct
|
||||
func (i *File) GetEditor() error {
|
||||
i.Language = editorLanguage(i.Extension)
|
||||
// If the editor will hold only content, leave now.
|
||||
if editorMode(i.Language) == "content" {
|
||||
@@ -194,7 +197,6 @@ func (i *file) getEditor() error {
|
||||
// If there is an error, just ignore it and return nil.
|
||||
// This way, the file can be served for editing.
|
||||
if err != nil {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -205,7 +207,7 @@ func (i *file) getEditor() error {
|
||||
|
||||
// GetFileType obtains the mimetype and converts it to a simple
|
||||
// type nomenclature.
|
||||
func (i *file) GetFileType(checkContent bool) error {
|
||||
func (i *File) GetFileType(checkContent bool) error {
|
||||
var content []byte
|
||||
var err error
|
||||
|
||||
@@ -258,14 +260,11 @@ func (i *file) GetFileType(checkContent bool) error {
|
||||
|
||||
// If the type isn't text (and is blob for example), it will check some
|
||||
// common types that are mistaken not to be text.
|
||||
for _, extension := range textExtensions {
|
||||
if strings.HasSuffix(i.Name, extension) {
|
||||
if isInTextExtensions(i.Name) {
|
||||
i.Type = "text"
|
||||
goto End
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
i.Type = "blob"
|
||||
}
|
||||
|
||||
End:
|
||||
// If the file type is text, save its content.
|
||||
@@ -283,7 +282,8 @@ End:
|
||||
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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -293,7 +293,7 @@ func (i file) Checksum(kind string) (string, error) {
|
||||
|
||||
var h hash.Hash
|
||||
|
||||
switch kind {
|
||||
switch algo {
|
||||
case "md5":
|
||||
h = md5.New()
|
||||
case "sha1":
|
||||
@@ -303,7 +303,7 @@ func (i file) Checksum(kind string) (string, error) {
|
||||
case "sha512":
|
||||
h = sha512.New()
|
||||
default:
|
||||
return "", errInvalidOption
|
||||
return "", ErrInvalidOption
|
||||
}
|
||||
|
||||
_, err = io.Copy(h, file)
|
||||
@@ -315,12 +315,12 @@ func (i file) Checksum(kind string) (string, error) {
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
// ApplySort applies the sort order using .Order and .Sort
|
||||
func (l listing) ApplySort() {
|
||||
func (l Listing) ApplySort() {
|
||||
// Check '.Order' to know how to sort
|
||||
if l.Order == "desc" {
|
||||
switch l.Sort {
|
||||
@@ -349,10 +349,10 @@ func (l listing) ApplySort() {
|
||||
}
|
||||
}
|
||||
|
||||
// Implement sorting for listing
|
||||
type byName listing
|
||||
type bySize listing
|
||||
type byModified listing
|
||||
// Implement sorting for Listing
|
||||
type byName Listing
|
||||
type bySize Listing
|
||||
type byModified Listing
|
||||
|
||||
// By Name
|
||||
func (l byName) Len() int {
|
||||
@@ -411,21 +411,38 @@ func (l byModified) Less(i, j int) bool {
|
||||
return iModified.Sub(jModified) < 0
|
||||
}
|
||||
|
||||
var textExtensions = [...]string{
|
||||
".md", ".markdown", ".mdown", ".mmark",
|
||||
".asciidoc", ".adoc", ".ad",
|
||||
".rst",
|
||||
".json", ".toml", ".yaml", ".csv", ".xml", ".rss", ".conf", ".ini",
|
||||
".tex", ".sty",
|
||||
".css", ".sass", ".scss",
|
||||
".js",
|
||||
".html",
|
||||
".txt", ".rtf",
|
||||
".sh", ".bash", ".ps1", ".bat", ".cmd",
|
||||
".php", ".pl", ".py",
|
||||
// textExtensions is the sorted list of text extensions which
|
||||
// can be edited.
|
||||
var textExtensions = []string{
|
||||
".ad", ".ada", ".adoc", ".asciidoc",
|
||||
".bas", ".bash", ".bat",
|
||||
".c", ".cc", ".cmd", ".conf", ".cpp", ".cr", ".cs", ".css", ".csv",
|
||||
".d",
|
||||
".f", ".f90",
|
||||
".h", ".hh", ".hpp", ".htaccess", ".html",
|
||||
".ini",
|
||||
".java", ".js", ".json",
|
||||
".markdown", ".md", ".mdown", ".mmark",
|
||||
".nim",
|
||||
".php", ".pl", ".ps1", ".py",
|
||||
".rss", ".rst", ".rtf",
|
||||
".sass", ".scss", ".sh", ".sty",
|
||||
".tex", ".tml", ".toml", ".txt",
|
||||
".vala", ".vapi",
|
||||
".xml",
|
||||
".yaml", ".yml",
|
||||
"Caddyfile",
|
||||
".c", ".cc", ".h", ".hh", ".cpp", ".hpp", ".f90",
|
||||
".f", ".bas", ".d", ".ada", ".nim", ".cr", ".java", ".cs", ".vala", ".vapi",
|
||||
}
|
||||
|
||||
// isInTextExtensions checks if a file can be edited by its extensions.
|
||||
func isInTextExtensions(name string) bool {
|
||||
search := filepath.Ext(name)
|
||||
if search == "" {
|
||||
search = name
|
||||
}
|
||||
|
||||
i := sort.SearchStrings(textExtensions, search)
|
||||
return i < len(textExtensions) && textExtensions[i] == search
|
||||
}
|
||||
|
||||
// hasRune checks if the file has the frontmatter rune
|
||||
|
||||
673
filemanager.go
673
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
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -64,38 +13,49 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
rice "github.com/GeertJohan/go.rice"
|
||||
"github.com/asdine/storm"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/GeertJohan/go.rice"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
const (
|
||||
// Version is the current File Manager version.
|
||||
Version = "1.4.4"
|
||||
|
||||
ListViewMode = "list"
|
||||
MosaicViewMode = "mosaic"
|
||||
)
|
||||
|
||||
var (
|
||||
errUserExist = errors.New("user already exists")
|
||||
errUserNotExist = errors.New("user does not exist")
|
||||
errEmptyRequest = errors.New("request body is empty")
|
||||
errEmptyPassword = errors.New("password is empty")
|
||||
errEmptyUsername = errors.New("username is empty")
|
||||
errEmptyScope = errors.New("scope is empty")
|
||||
errWrongDataType = errors.New("wrong data type")
|
||||
errInvalidUpdateField = errors.New("invalid field to update")
|
||||
ErrExist = errors.New("the resource already exists")
|
||||
ErrNotExist = errors.New("the resource does not exist")
|
||||
ErrEmptyRequest = errors.New("request body is empty")
|
||||
ErrEmptyPassword = errors.New("password is empty")
|
||||
ErrEmptyUsername = errors.New("username is empty")
|
||||
ErrEmptyScope = errors.New("scope is empty")
|
||||
ErrWrongDataType = errors.New("wrong data type")
|
||||
ErrInvalidUpdateField = errors.New("invalid field to update")
|
||||
ErrInvalidOption = errors.New("invalid option")
|
||||
)
|
||||
|
||||
// FileManager is a file manager instance. It should be creating using the
|
||||
// 'New' function and not directly.
|
||||
type FileManager struct {
|
||||
// The BoltDB database for this instance.
|
||||
db *storm.DB
|
||||
// Cron job to manage schedulings.
|
||||
Cron *cron.Cron
|
||||
|
||||
// The key used to sign the JWT tokens.
|
||||
key []byte
|
||||
Key []byte
|
||||
|
||||
// The static assets.
|
||||
assets *rice.Box
|
||||
Assets *rice.Box
|
||||
|
||||
// Job cron.
|
||||
cron *cron.Cron
|
||||
// The Store is used to manage users, shareable links and
|
||||
// other stuff that is saved on the database.
|
||||
Store *Store
|
||||
|
||||
// PrefixURL is a part of the URL that is already trimmed from the request URL before it
|
||||
// arrives to our handlers. It may be useful when using File Manager as a middleware
|
||||
@@ -111,174 +71,130 @@ type FileManager struct {
|
||||
// there will only exist one user, called "admin".
|
||||
NoAuth bool
|
||||
|
||||
// staticgen is the name of the current static website generator.
|
||||
staticgen string
|
||||
// ReCaptcha Site key and secret.
|
||||
ReCaptchaKey string
|
||||
ReCaptchaSecret string
|
||||
|
||||
// StaticGen is the static websit generator handler.
|
||||
StaticGen StaticGen
|
||||
|
||||
// The Default User needed to build the New User page.
|
||||
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.
|
||||
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.
|
||||
type Command func(r *http.Request, m *FileManager, u *User) error
|
||||
|
||||
// User contains the configuration for each user.
|
||||
type User struct {
|
||||
// ID is the required primary key with auto increment0
|
||||
ID int `storm:"id,increment"`
|
||||
// FSBuilder is the File System Builder.
|
||||
type FSBuilder func(scope string) FileSystem
|
||||
|
||||
// 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"`
|
||||
|
||||
// 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) {
|
||||
// Setup loads the configuration from the database and configures
|
||||
// the Assets and the Cron job. It must always be run after
|
||||
// creating a File Manager object.
|
||||
func (m *FileManager) Setup() error {
|
||||
// Creates a new File Manager instance with the Users
|
||||
// map and Assets box.
|
||||
m := &FileManager{
|
||||
Users: map[string]*User{},
|
||||
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
|
||||
}
|
||||
m.Assets = rice.MustFindBox("./assets/dist")
|
||||
m.Cron = cron.New()
|
||||
|
||||
// Tries to get the encryption key from the database.
|
||||
// If it doesn't exist, create a new one of 256 bits.
|
||||
err = db.Get("config", "key", &m.key)
|
||||
if err != nil && err == storm.ErrNotFound {
|
||||
err := m.Store.Config.Get("key", &m.Key)
|
||||
if err != nil && err == ErrNotExist {
|
||||
var bytes []byte
|
||||
bytes, err = generateRandomBytes(64)
|
||||
bytes, err = GenerateRandomBytes(64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
m.key = bytes
|
||||
err = db.Set("config", "key", m.key)
|
||||
m.Key = bytes
|
||||
err = m.Store.Config.Save("key", m.Key)
|
||||
}
|
||||
|
||||
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.
|
||||
// If they don't exist, initialize them.
|
||||
err = db.Get("config", "commands", &m.Commands)
|
||||
if err != nil && err == storm.ErrNotFound {
|
||||
m.Commands = map[string][]string{
|
||||
"before_save": {},
|
||||
"after_save": {},
|
||||
"before_publish": {},
|
||||
"after_publish": {},
|
||||
err = m.Store.Config.Get("commands", &m.Commands)
|
||||
|
||||
if err == nil {
|
||||
// Add hypothetically new command handlers.
|
||||
for _, command := range commandEvents {
|
||||
if _, ok := m.Commands[command]; ok {
|
||||
continue
|
||||
}
|
||||
err = db.Set("config", "commands", m.Commands)
|
||||
|
||||
m.Commands[command] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// Tries to fetch the users from the database and if there are
|
||||
// any, add them to the current File Manager instance.
|
||||
var users []User
|
||||
err = db.All(&users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range users {
|
||||
m.Users[users[i].Username] = &users[i]
|
||||
// Tries to fetch the users from the database.
|
||||
users, err := m.Store.Users.Gets(m.NewFS)
|
||||
if err != nil && err != ErrNotExist {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.
|
||||
if len(users) == 0 {
|
||||
u := base
|
||||
u := *m.DefaultUser
|
||||
u.Username = "admin"
|
||||
|
||||
// Hashes the password.
|
||||
u.Password, err = hashPassword("admin")
|
||||
u.Password, err = HashPassword("admin")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// The first user must be an administrator.
|
||||
@@ -289,25 +205,26 @@ func New(database string, base User) (*FileManager, error) {
|
||||
u.AllowPublish = true
|
||||
|
||||
// Saves the user to the database.
|
||||
if err := db.Save(&u); err != nil {
|
||||
return nil, err
|
||||
if err := m.Store.Users.Save(&u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
m.Users[u.Username] = &u
|
||||
// TODO: remove this after 1.5
|
||||
for _, user := range users {
|
||||
if user.ViewMode != ListViewMode && user.ViewMode != MosaicViewMode {
|
||||
user.ViewMode = ListViewMode
|
||||
m.Store.Users.Update(user, "ViewMode")
|
||||
}
|
||||
}
|
||||
|
||||
// Attaches db to this File Manager instance.
|
||||
m.db = db
|
||||
m.DefaultUser.Username = ""
|
||||
m.DefaultUser.Password = ""
|
||||
|
||||
// Create the default user, making a copy of the base.
|
||||
base.Username = ""
|
||||
base.Password = ""
|
||||
m.DefaultUser = &base
|
||||
m.Cron.AddFunc("@hourly", m.ShareCleaner)
|
||||
m.Cron.Start()
|
||||
|
||||
m.cron.AddFunc("@hourly", m.shareCleaner)
|
||||
m.cron.Start()
|
||||
|
||||
return m, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// RootURL returns the actual URL where
|
||||
@@ -334,95 +251,32 @@ func (m *FileManager) SetBaseURL(url string) {
|
||||
m.BaseURL = strings.TrimSuffix(url, "/")
|
||||
}
|
||||
|
||||
// ServeHTTP handles the request.
|
||||
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
code, err := serveHTTP(&RequestContext{
|
||||
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 {
|
||||
// Attach attaches a static generator to the current File Manager.
|
||||
func (m *FileManager) Attach(s StaticGen) error {
|
||||
if reflect.TypeOf(s).Kind() != reflect.Ptr {
|
||||
return errors.New("data should be a pointer to interface, not interface")
|
||||
}
|
||||
|
||||
if h, ok := data.(*Hugo); ok {
|
||||
return m.enableHugo(h)
|
||||
}
|
||||
|
||||
if j, ok := data.(*Jekyll); ok {
|
||||
return m.enableJekyll(j)
|
||||
}
|
||||
|
||||
return errors.New("unknown static website generator")
|
||||
}
|
||||
|
||||
func (m *FileManager) enableHugo(h *Hugo) error {
|
||||
if err := h.find(); err != nil {
|
||||
err := s.Setup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.staticgen = "hugo"
|
||||
m.StaticGen = h
|
||||
m.StaticGen = s
|
||||
|
||||
err := m.db.Get("staticgen", "hugo", h)
|
||||
if err != nil && err == storm.ErrNotFound {
|
||||
err = m.db.Set("staticgen", "hugo", *h)
|
||||
err = m.Store.Config.Get("staticgen_"+s.Name(), s)
|
||||
if err == ErrNotExist {
|
||||
return m.Store.Config.Save("staticgen_"+s.Name(), s)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *FileManager) enableJekyll(j *Jekyll) error {
|
||||
if err := j.find(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(j.Args) == 0 {
|
||||
j.Args = []string{"build"}
|
||||
}
|
||||
|
||||
if j.Args[0] != "build" {
|
||||
j.Args = append([]string{"build"}, j.Args...)
|
||||
}
|
||||
|
||||
m.staticgen = "jekyll"
|
||||
m.StaticGen = j
|
||||
|
||||
err := m.db.Get("staticgen", "jekyll", j)
|
||||
if err != nil && err == storm.ErrNotFound {
|
||||
err = m.db.Set("staticgen", "jekyll", *j)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shareCleaner removes sharing links that are no longer active.
|
||||
// ShareCleaner removes sharing links that are no longer active.
|
||||
// This function is set to run periodically.
|
||||
func (m FileManager) shareCleaner() {
|
||||
var links []shareLink
|
||||
|
||||
func (m FileManager) ShareCleaner() {
|
||||
// Get all links.
|
||||
err := m.db.All(&links)
|
||||
links, err := m.Store.Share.Gets()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
@@ -431,7 +285,7 @@ func (m FileManager) shareCleaner() {
|
||||
// Find the expired ones.
|
||||
for i := range links {
|
||||
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 {
|
||||
log.Print(err)
|
||||
}
|
||||
@@ -439,39 +293,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.
|
||||
func (m FileManager) Runner(event string, path string) error {
|
||||
func (m FileManager) Runner(event string, path string, destination string, user *User) error {
|
||||
commands := []string{}
|
||||
|
||||
// Get the commands from the File Manager instance itself.
|
||||
@@ -496,7 +319,15 @@ func (m FileManager) Runner(event string, path string) error {
|
||||
}
|
||||
|
||||
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.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -518,3 +349,217 @@ func (m FileManager) Runner(event string, path string) error {
|
||||
|
||||
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: "",
|
||||
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,66 @@
|
||||
package filemanager
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go/request"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
// authHandler proccesses the authentication for the user.
|
||||
func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
const reCaptchaAPI = "https://www.google.com/recaptcha/api/siteverify"
|
||||
|
||||
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) {
|
||||
body := url.Values{}
|
||||
body.Set("secret", secret)
|
||||
body.Add("response", response)
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
resp, err := client.Post(reCaptchaAPI, "application/x-www-form-urlencoded", strings.NewReader(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 processes the authentication for the user.
|
||||
func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// NoAuth instances shouldn't call this method.
|
||||
if c.NoAuth {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Receive the credentials from the request and unmarshal them.
|
||||
var cred User
|
||||
var cred cred
|
||||
if r.Body == nil {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -31,14 +70,26 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Checks if the user exists.
|
||||
u, ok := c.Users[cred.Username]
|
||||
// If ReCaptcha is enabled, check the code.
|
||||
if len(c.ReCaptchaSecret) > 0 {
|
||||
ok, err := reCaptcha(c.ReCaptchaSecret, cred.ReCaptcha)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the user exists.
|
||||
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Checks if the password is correct.
|
||||
if !checkPasswordHash(cred.Password, u.Password) {
|
||||
if !fm.CheckPasswordHash(cred.Password, u.Password) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
@@ -48,7 +99,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
|
||||
|
||||
// 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.
|
||||
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)
|
||||
if !ok {
|
||||
return http.StatusForbidden, nil
|
||||
@@ -60,23 +111,21 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// claims is the JWT claims.
|
||||
type claims struct {
|
||||
User
|
||||
NoAuth bool `json:"noAuth"`
|
||||
fm.User
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// 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
|
||||
// hash so it never arrives to the user.
|
||||
u := User{}
|
||||
u := fm.User{}
|
||||
u = *c.User
|
||||
u.Password = ""
|
||||
|
||||
// Builds the claims.
|
||||
claims := claims{
|
||||
u,
|
||||
c.NoAuth,
|
||||
jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
|
||||
Issuer: "File Manager",
|
||||
@@ -85,7 +134,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
|
||||
|
||||
// Creates the token and signs it.
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, err := token.SignedString(c.key)
|
||||
signed, err := token.SignedString(c.Key)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
@@ -119,15 +168,16 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
|
||||
|
||||
// validateAuth is used to validate the authentication and returns the
|
||||
// User if it is valid.
|
||||
func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
|
||||
func validateAuth(c *fm.Context, r *http.Request) (bool, *fm.User) {
|
||||
if c.NoAuth {
|
||||
c.User = c.DefaultUser
|
||||
return true, c.User
|
||||
}
|
||||
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return c.key, nil
|
||||
return c.Key, nil
|
||||
}
|
||||
|
||||
var claims claims
|
||||
token, err := request.ParseFromRequestWithClaims(r,
|
||||
extractor{},
|
||||
@@ -139,38 +189,11 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
u, ok := c.Users[claims.User.Username]
|
||||
if !ok {
|
||||
u, err := c.Store.Users.Get(claims.User.ID, c.NewFS)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
c.User = 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 (
|
||||
"io"
|
||||
@@ -9,28 +9,21 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
fm "github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/archiver"
|
||||
)
|
||||
|
||||
// downloadHandler creates an archive in one of the supported formats (zip, tar,
|
||||
// tar.gz or tar.bz2) and sends it to be downloaded.
|
||||
func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
query := r.URL.Query().Get("format")
|
||||
|
||||
func downloadHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// If the file isn't a directory, serve it using http.ServeFile. We display it
|
||||
// inline if it is requested.
|
||||
if !c.File.IsDir {
|
||||
if r.URL.Query().Get("inline") == "true" {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name)
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, c.File.Path)
|
||||
return 0, nil
|
||||
return downloadFileHandler(c, w, r)
|
||||
}
|
||||
|
||||
query := r.URL.Query().Get("format")
|
||||
files := []string{}
|
||||
names := strings.Split(r.URL.Query().Get("files"), ",")
|
||||
|
||||
@@ -106,7 +99,29 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+name)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
|
||||
_, err = io.Copy(w, file)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func downloadFileHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if r.URL.Query().Get("inline") == "true" {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+c.File.Name+`"`)
|
||||
}
|
||||
|
||||
file, err := os.Open(c.File.Path)
|
||||
defer file.Close()
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, file)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
@@ -1,29 +1,45 @@
|
||||
package filemanager
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
// RequestContext contains the needed information to make handlers work.
|
||||
type RequestContext struct {
|
||||
*FileManager
|
||||
User *User
|
||||
File *file
|
||||
// On API handlers, Router is the APi handler we want.
|
||||
Router string
|
||||
// Handler returns a function compatible with http.HandleFunc.
|
||||
func Handler(m *fm.FileManager) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
code, err := serve(&fm.Context{
|
||||
FileManager: m,
|
||||
User: nil,
|
||||
File: nil,
|
||||
}, w, r)
|
||||
|
||||
if code >= 400 {
|
||||
w.WriteHeader(code)
|
||||
|
||||
txt := http.StatusText(code)
|
||||
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
|
||||
w.Write([]byte(txt + "\n"))
|
||||
}
|
||||
|
||||
// serveHTTP is the main entry point of this HTML application.
|
||||
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// serve is the main entry point of this HTML application.
|
||||
func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
|
||||
// returns a 404 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)
|
||||
|
||||
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
|
||||
@@ -35,11 +51,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
// Check if this request is made to the service worker. If so,
|
||||
// pass it through a template to add the needed variables.
|
||||
if r.URL.Path == "/sw.js" {
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.assets.MustString("sw.js"),
|
||||
"application/javascript",
|
||||
)
|
||||
return renderFile(c, w, "sw.js")
|
||||
}
|
||||
|
||||
// Checks if this request is made to the static assets folder. If so, and
|
||||
@@ -67,7 +79,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
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/")
|
||||
return sharePage(c, w, r)
|
||||
}
|
||||
@@ -77,29 +89,21 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
w.Header().Set("x-content-type", "nosniff")
|
||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.assets.MustString("index.html"),
|
||||
"text/html",
|
||||
)
|
||||
return renderFile(c, w, "index.html")
|
||||
}
|
||||
|
||||
// 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" {
|
||||
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
|
||||
http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.assets.MustString("static/manifest.json"),
|
||||
"application/json",
|
||||
)
|
||||
return renderFile(c, w, "static/manifest.json")
|
||||
}
|
||||
|
||||
// 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" {
|
||||
return authHandler(c, w, r)
|
||||
}
|
||||
@@ -135,9 +139,9 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
|
||||
if c.Router == "checksum" || c.Router == "download" {
|
||||
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 {
|
||||
return errorToHTTP(err, false), err
|
||||
return ErrorToHTTP(err, false), err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,11 +173,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.
|
||||
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")
|
||||
|
||||
val, err := c.File.Checksum(query)
|
||||
if err == errInvalidOption {
|
||||
if err == fm.ErrInvalidOption {
|
||||
return http.StatusBadRequest, err
|
||||
} else if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
@@ -201,14 +205,39 @@ func splitURL(path string) (string, string) {
|
||||
}
|
||||
|
||||
// renderFile renders a file using a template with some needed variables.
|
||||
func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentType string) (int, error) {
|
||||
tpl := template.Must(template.New("file").Parse(file))
|
||||
func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) {
|
||||
tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file)))
|
||||
|
||||
var contentType string
|
||||
switch filepath.Ext(file) {
|
||||
case ".html":
|
||||
contentType = "text/html"
|
||||
case ".js":
|
||||
contentType = "application/javascript"
|
||||
case ".json":
|
||||
contentType = "application/json"
|
||||
default:
|
||||
contentType = "text"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
||||
|
||||
err := tpl.Execute(w, map[string]interface{}{
|
||||
data := map[string]interface{}{
|
||||
"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 {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -216,15 +245,12 @@ func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentTy
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var s shareLink
|
||||
err := c.db.One("Hash", r.URL.Path, &s)
|
||||
if err == storm.ErrNotFound {
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.assets.MustString("static/share/404.html"),
|
||||
"text/html",
|
||||
)
|
||||
// sharePage build the share page.
|
||||
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
s, err := c.Store.Share.Get(r.URL.Path)
|
||||
if err == fm.ErrNotExist {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return renderFile(c, w, "static/share/404.html")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -232,22 +258,20 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
}
|
||||
|
||||
if s.Expires && s.ExpireDate.Before(time.Now()) {
|
||||
c.db.DeleteStruct(&s)
|
||||
return renderFile(
|
||||
c, w,
|
||||
c.assets.MustString("static/share/404.html"),
|
||||
"text/html",
|
||||
)
|
||||
c.Store.Share.Delete(s.Hash)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return renderFile(c, w, "static/share/404.html")
|
||||
}
|
||||
|
||||
r.URL.Path = s.Path
|
||||
|
||||
info, err := os.Stat(s.Path)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
c.Store.Share.Delete(s.Hash)
|
||||
return ErrorToHTTP(err, false), err
|
||||
}
|
||||
|
||||
c.File = &file{
|
||||
c.File = &fm.File{
|
||||
Path: s.Path,
|
||||
Name: info.Name(),
|
||||
ModTime: info.ModTime(),
|
||||
@@ -259,7 +283,7 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
dl := r.URL.Query().Get("dl")
|
||||
|
||||
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")
|
||||
|
||||
err := tpl.Execute(w, map[string]interface{}{
|
||||
@@ -299,8 +323,8 @@ func matchURL(first, second string) bool {
|
||||
return strings.HasPrefix(first, second)
|
||||
}
|
||||
|
||||
// errorToHTTP converts errors to HTTP Status Code.
|
||||
func errorToHTTP(err error, gone bool) int {
|
||||
// ErrorToHTTP converts errors to HTTP Status Code.
|
||||
func ErrorToHTTP(err error, gone bool) int {
|
||||
switch {
|
||||
case err == nil:
|
||||
return http.StatusOK
|
||||
@@ -1,4 +1,4 @@
|
||||
package filemanager
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
fm "github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/fileutils"
|
||||
)
|
||||
|
||||
@@ -26,7 +27,7 @@ func sanitizeURL(url string) string {
|
||||
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)
|
||||
|
||||
switch r.Method {
|
||||
@@ -36,8 +37,8 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
return resourceDeleteHandler(c, w, r)
|
||||
case http.MethodPut:
|
||||
// Before save command handler.
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
if err := c.Runner("before_save", path); err != nil {
|
||||
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||
if err := c.Runner("before_save", path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -61,11 +62,11 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
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.
|
||||
f, err := getInfo(r.URL, c.FileManager, c.User)
|
||||
f, err := fm.GetInfo(r.URL, c.FileManager, c.User)
|
||||
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,
|
||||
@@ -82,7 +83,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// Tries to get the file type.
|
||||
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
|
||||
@@ -96,23 +97,23 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
f.Kind = "editor"
|
||||
|
||||
// Tries to get the editor data.
|
||||
if err = f.getEditor(); err != nil {
|
||||
if err = f.GetEditor(); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
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.Kind = "listing"
|
||||
|
||||
// Tries to get the listing data.
|
||||
if err := f.getListing(c, r); err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
if err := f.GetListing(c.User, r); err != nil {
|
||||
return ErrorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
listing := f.listing
|
||||
listing := f.Listing
|
||||
|
||||
// Defines the cookie scope.
|
||||
cookieScope := c.RootURL()
|
||||
@@ -129,27 +130,35 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
|
||||
}
|
||||
|
||||
listing.ApplySort()
|
||||
listing.Display = displayMode(w, r, cookieScope)
|
||||
|
||||
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.
|
||||
if r.URL.Path == "/" || !c.User.AllowEdit {
|
||||
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.
|
||||
err := c.User.FileSystem.RemoveAll(r.URL.Path)
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
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.
|
||||
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
|
||||
@@ -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.
|
||||
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
return ErrorToHTTP(err, false), err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Copies the new content for the file.
|
||||
_, err = io.Copy(f, r.Body)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
return ErrorToHTTP(err, false), err
|
||||
}
|
||||
|
||||
// Gets the info about the file.
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
return ErrorToHTTP(err, false), err
|
||||
}
|
||||
|
||||
// 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.
|
||||
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
schedule := r.Header.Get("Schedule")
|
||||
|
||||
@@ -241,7 +261,7 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
|
||||
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)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
@@ -251,11 +271,11 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||
|
||||
// Before save command handler.
|
||||
if err := c.Runner("before_publish", path); err != nil {
|
||||
if err := c.Runner("before_publish", path, "", c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -265,7 +285,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -273,7 +293,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -282,7 +302,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
|
||||
action := r.Header.Get("Action")
|
||||
dst, err := url.QueryUnescape(dst)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, true), err
|
||||
return ErrorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
src := r.URL.Path
|
||||
@@ -292,38 +312,34 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Fire the after trigger.
|
||||
if err := c.Runner("after_copy", src, dst, c.User); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
} 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)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
return ErrorToHTTP(err, true), err
|
||||
}
|
||||
|
||||
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
|
||||
@@ -1,4 +1,4 @@
|
||||
package filemanager
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
fm "github.com/hacdias/filemanager"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type modifySettingsRequest struct {
|
||||
*modifyRequest
|
||||
Data struct {
|
||||
CSS string `json:"css"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
StaticGen map[string]interface{} `json:"staticGen"`
|
||||
} `json:"data"`
|
||||
@@ -26,7 +28,7 @@ type option struct {
|
||||
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
|
||||
// Checks if the request body is empty.
|
||||
if r.Body == nil {
|
||||
return nil, errEmptyRequest
|
||||
return nil, fm.ErrEmptyRequest
|
||||
}
|
||||
|
||||
// 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.
|
||||
if mod.What != "settings" {
|
||||
return nil, errWrongDataType
|
||||
return nil, fm.ErrWrongDataType
|
||||
}
|
||||
|
||||
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 != "/" {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
@@ -60,11 +62,12 @@ func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
type settingsGetRequest struct {
|
||||
CSS string `json:"css"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
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 {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -72,6 +75,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
result := &settingsGetRequest{
|
||||
Commands: c.Commands,
|
||||
StaticGen: []option{},
|
||||
CSS: c.CSS,
|
||||
}
|
||||
|
||||
if c.StaticGen != nil {
|
||||
@@ -93,7 +97,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
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 {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -102,9 +106,10 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// Update the 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
|
||||
}
|
||||
|
||||
@@ -112,6 +117,16 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
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.
|
||||
if mod.Which == "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
|
||||
}
|
||||
|
||||
err = c.db.Set("staticgen", c.staticgen, c.StaticGen)
|
||||
err = c.Store.Config.Save("staticgen_"+c.StaticGen.Name(), c.StaticGen)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package filemanager
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@@ -8,18 +8,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
"github.com/asdine/storm/q"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
type shareLink struct {
|
||||
Hash string `json:"hash" storm:"id,index"`
|
||||
Path string `json:"path" storm:"index"`
|
||||
Expires bool `json:"expires"`
|
||||
ExpireDate time.Time `json:"expireDate"`
|
||||
}
|
||||
|
||||
func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
func shareHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
r.URL.Path = sanitizeURL(r.URL.Path)
|
||||
|
||||
switch r.Method {
|
||||
@@ -34,14 +26,10 @@ func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
|
||||
return http.StatusNotImplemented, nil
|
||||
}
|
||||
|
||||
func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var (
|
||||
s []*shareLink
|
||||
path = filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
)
|
||||
|
||||
err := c.db.Find("Path", path, &s)
|
||||
if err == storm.ErrNotFound {
|
||||
func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||
s, err := c.Store.Share.GetByPath(path)
|
||||
if err == fm.ErrNotExist {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
@@ -51,37 +39,42 @@ func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
for i, link := range s {
|
||||
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:]...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(s) == 0 {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
return renderJSON(w, s)
|
||||
}
|
||||
|
||||
func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
path := filepath.Join(c.User.Scope, r.URL.Path)
|
||||
|
||||
var s shareLink
|
||||
var s *fm.ShareLink
|
||||
expire := r.URL.Query().Get("expires")
|
||||
unit := r.URL.Query().Get("unit")
|
||||
|
||||
if expire == "" {
|
||||
err := c.db.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&s)
|
||||
var err error
|
||||
s, err = c.Store.Share.GetPermanent(path)
|
||||
if err == nil {
|
||||
w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := generateRandomBytes(32)
|
||||
bytes, err := fm.GenerateRandomBytes(32)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
str := hex.EncodeToString(bytes)
|
||||
|
||||
s = shareLink{
|
||||
s = &fm.ShareLink{
|
||||
Path: path,
|
||||
Hash: str,
|
||||
Expires: expire != "",
|
||||
@@ -108,19 +101,16 @@ func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
s.ExpireDate = time.Now().Add(add)
|
||||
}
|
||||
|
||||
err = c.db.Save(&s)
|
||||
if err != nil {
|
||||
if err := c.Store.Share.Save(s); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return renderJSON(w, s)
|
||||
}
|
||||
|
||||
func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var s shareLink
|
||||
|
||||
err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)
|
||||
if err == storm.ErrNotFound {
|
||||
func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
s, err := c.Store.Share.Get(strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if err == fm.ErrNotExist {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
@@ -128,7 +118,7 @@ func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
err = c.db.DeleteStruct(&s)
|
||||
err = c.Store.Share.Delete(s.Hash)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package filemanager
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
type modifyRequest struct {
|
||||
@@ -19,12 +19,12 @@ type modifyRequest struct {
|
||||
|
||||
type modifyUserRequest struct {
|
||||
*modifyRequest
|
||||
Data *User `json:"data"`
|
||||
Data *fm.User `json:"data"`
|
||||
}
|
||||
|
||||
// usersHandler is the entry point of the users API. It's just a router
|
||||
// to send the request to its
|
||||
func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
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
|
||||
// request, then return forbidden.
|
||||
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
|
||||
// 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) {
|
||||
// Obtains the ID in string from the URL and converts
|
||||
// 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
|
||||
// body. If the body is empty or the JSON is invalid, it
|
||||
// returns an error.
|
||||
func getUser(r *http.Request) (*User, string, error) {
|
||||
// returns an fm.Error.
|
||||
func getUser(c *fm.Context, r *http.Request) (*fm.User, string, error) {
|
||||
// Checks if the request body is empty.
|
||||
if r.Body == nil {
|
||||
return nil, "", errEmptyRequest
|
||||
return nil, "", fm.ErrEmptyRequest
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
if r.URL.Path == "/base" {
|
||||
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.
|
||||
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 {
|
||||
// Copies the user info and removes its
|
||||
// password so it won't be sent to the
|
||||
// front-end.
|
||||
u := *user
|
||||
for _, u := range users {
|
||||
// Removes the user password so it won't
|
||||
// be sent to the front-end.
|
||||
u.Password = ""
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
@@ -116,54 +117,52 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Searches for the user and prints the one who matches.
|
||||
for _, user := range c.Users {
|
||||
if user.ID != id {
|
||||
continue
|
||||
u, err := c.Store.Users.Get(id, c.NewFS)
|
||||
if err == fm.ErrExist {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
u := *user
|
||||
u.Password = ""
|
||||
return renderJSON(w, u)
|
||||
}
|
||||
|
||||
// If there aren't any matches, return not found.
|
||||
return http.StatusNotFound, errUserNotExist
|
||||
}
|
||||
|
||||
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 != "/" {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
u, _, err := getUser(r)
|
||||
u, _, err := getUser(c, r)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// Checks if username isn't empty.
|
||||
if u.Username == "" {
|
||||
return http.StatusBadRequest, errEmptyUsername
|
||||
return http.StatusBadRequest, fm.ErrEmptyUsername
|
||||
}
|
||||
|
||||
// Checks if filesystem isn't empty.
|
||||
if u.FileSystem == "" {
|
||||
return http.StatusBadRequest, errEmptyScope
|
||||
// Checks if scope isn't empty.
|
||||
if u.Scope == "" {
|
||||
return http.StatusBadRequest, fm.ErrEmptyScope
|
||||
}
|
||||
|
||||
// Checks if password isn't empty.
|
||||
if u.Password == "" {
|
||||
return http.StatusBadRequest, errEmptyPassword
|
||||
}
|
||||
|
||||
// The username, password and scope cannot be empty.
|
||||
if u.Username == "" || u.Password == "" || u.FileSystem == "" {
|
||||
return http.StatusBadRequest, errors.New("username, password or scope is empty")
|
||||
return http.StatusBadRequest, fm.ErrEmptyPassword
|
||||
}
|
||||
|
||||
// Initialize rules if they're not initialized.
|
||||
if u.Rules == nil {
|
||||
u.Rules = []*Rule{}
|
||||
u.Rules = []*fm.Rule{}
|
||||
}
|
||||
|
||||
// If the view mode is empty, initialize with the default one.
|
||||
if u.ViewMode == "" {
|
||||
u.ViewMode = c.DefaultUser.ViewMode
|
||||
}
|
||||
|
||||
// Initialize commands if not initialized.
|
||||
@@ -177,33 +176,31 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Hashes the password.
|
||||
pw, err := hashPassword(u.Password)
|
||||
pw, err := fm.HashPassword(u.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
u.Password = pw
|
||||
u.ViewMode = fm.MosaicViewMode
|
||||
|
||||
// Saves the user to the database.
|
||||
err = c.db.Save(u)
|
||||
if err == storm.ErrAlreadyExists {
|
||||
return http.StatusConflict, errUserExist
|
||||
err = c.Store.Users.Save(u)
|
||||
if err == fm.ErrExist {
|
||||
return http.StatusConflict, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Saves the user to the memory.
|
||||
c.Users[u.Username] = u
|
||||
|
||||
// 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)
|
||||
return 0, nil
|
||||
}
|
||||
@@ -231,7 +228,7 @@ func checkFS(path string) (int, error) {
|
||||
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 == "/" {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
@@ -242,27 +239,19 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// Deletes the user from the database.
|
||||
err = c.db.DeleteStruct(&User{ID: id})
|
||||
if err == storm.ErrNotFound {
|
||||
return http.StatusNotFound, errUserNotExist
|
||||
err = c.Store.Users.Delete(id)
|
||||
if err == fm.ErrNotExist {
|
||||
return http.StatusNotFound, fm.ErrNotExist
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
if r.URL.Path == "/" {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
@@ -280,21 +269,25 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Gets the user from the request body.
|
||||
u, which, err := getUser(r)
|
||||
u, which, err := getUser(c, r)
|
||||
if err != nil {
|
||||
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.
|
||||
if which == "partial" {
|
||||
c.User.CSS = u.CSS
|
||||
c.User.Locale = u.Locale
|
||||
err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
c.User.ViewMode = u.ViewMode
|
||||
|
||||
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 {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -305,16 +298,19 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
// Updates the Password.
|
||||
if which == "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 {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
c.User.Password = pw
|
||||
err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
|
||||
err = c.Store.Users.Update(c.User, "Password")
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -324,27 +320,27 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// If can only be all.
|
||||
if which != "all" {
|
||||
return http.StatusBadRequest, errInvalidUpdateField
|
||||
return http.StatusBadRequest, fm.ErrInvalidUpdateField
|
||||
}
|
||||
|
||||
// Checks if username isn't empty.
|
||||
if u.Username == "" {
|
||||
return http.StatusBadRequest, errEmptyUsername
|
||||
return http.StatusBadRequest, fm.ErrEmptyUsername
|
||||
}
|
||||
|
||||
// Checks if filesystem isn't empty.
|
||||
if u.FileSystem == "" {
|
||||
return http.StatusBadRequest, errEmptyScope
|
||||
if u.Scope == "" {
|
||||
return http.StatusBadRequest, fm.ErrEmptyScope
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Initialize rules if they're not initialized.
|
||||
if u.Rules == nil {
|
||||
u.Rules = []*Rule{}
|
||||
u.Rules = []*fm.Rule{}
|
||||
}
|
||||
|
||||
// Initialize commands if not initialized.
|
||||
@@ -353,22 +349,20 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Gets the current saved user from the in-memory map.
|
||||
var suser *User
|
||||
for _, user := range c.Users {
|
||||
if user.ID == id {
|
||||
suser = user
|
||||
break
|
||||
}
|
||||
}
|
||||
if suser == nil {
|
||||
suser, err := c.Store.Users.Get(id, c.NewFS)
|
||||
if err == fm.ErrNotExist {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
u.ID = id
|
||||
|
||||
// Changes the password if the request wants it.
|
||||
if u.Password != "" {
|
||||
pw, err := hashPassword(u.Password)
|
||||
pw, err := fm.HashPassword(u.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -380,17 +374,10 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Updates the whole User struct because we always are supposed
|
||||
// to send a new entire object.
|
||||
err = c.db.Save(u)
|
||||
err = c.Store.Users.Update(u)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// If the user changed the username, delete the old user
|
||||
// from the in-memory user map.
|
||||
if suser.Username != u.Username {
|
||||
delete(c.Users, suser.Username)
|
||||
}
|
||||
|
||||
c.Users[u.Username] = u
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package filemanager
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
fm "github.com/hacdias/filemanager"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
@@ -26,8 +27,8 @@ var (
|
||||
)
|
||||
|
||||
// command handles the requests for VCS related commands: git, svn and mercurial
|
||||
func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Upgrades the connection to a websocket and checks for errors.
|
||||
func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Upgrades the connection to a websocket and checks for fm.Errors.
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
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.
|
||||
path := string(c.User.FileSystem) + "/" + r.URL.Path
|
||||
path := c.User.Scope + "/" + r.URL.Path
|
||||
path = filepath.Clean(path)
|
||||
buff := new(bytes.Buffer)
|
||||
|
||||
@@ -91,7 +92,7 @@ func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, er
|
||||
cmd.Stderr = buff
|
||||
cmd.Stdout = buff
|
||||
|
||||
// Starts the command and checks for errors.
|
||||
// Starts the command and checks for fm.Errors.
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
@@ -239,8 +240,8 @@ func parseSearch(value string) *searchOptions {
|
||||
}
|
||||
|
||||
// search searches for a file or directory.
|
||||
func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Upgrades the connection to a websocket and checks for errors.
|
||||
func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Upgrades the connection to a websocket and checks for fm.Errors.
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -269,7 +270,7 @@ func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, err
|
||||
search = parseSearch(value)
|
||||
scope := strings.TrimPrefix(r.URL.Path, "/")
|
||||
scope = "/" + scope
|
||||
scope = string(c.User.FileSystem) + scope
|
||||
scope = c.User.Scope + scope
|
||||
scope = strings.Replace(scope, "\\", "/", -1)
|
||||
scope = filepath.Clean(scope)
|
||||
|
||||
94
package.json
94
package.json
@@ -10,61 +10,63 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^1.7.1",
|
||||
"codemirror": "^5.27.4",
|
||||
"filesize": "^3.5.10",
|
||||
"moment": "^2.18.1",
|
||||
"codemirror": "^5.31.0",
|
||||
"filesize": "^3.5.11",
|
||||
"js-base64": "^2.4.0",
|
||||
"moment": "^2.19.2",
|
||||
"normalize.css": "^7.0.0",
|
||||
"vue": "^2.3.3",
|
||||
"vue-i18n": "^7.1.0",
|
||||
"vue-router": "^2.7.0",
|
||||
"vuex": "^2.3.1"
|
||||
"noty": "^3.1.4",
|
||||
"vue": "^2.5.8",
|
||||
"vue-i18n": "^7.3.4",
|
||||
"vue-router": "^3.0.1",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.2",
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-preset-env": "^1.3.2",
|
||||
"babel-preset-stage-2": "^6.22.0",
|
||||
"babel-register": "^6.22.0",
|
||||
"chalk": "^2.0.1",
|
||||
"connect-history-api-fallback": "^1.3.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"eslint": "^4.3.0",
|
||||
"autoprefixer": "^7.1.6",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.0.2",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"chalk": "^2.3.0",
|
||||
"connect-history-api-fallback": "^1.5.0",
|
||||
"copy-webpack-plugin": "^4.2.1",
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^4.15.0",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-friendly-formatter": "^3.0.0",
|
||||
"eslint-loader": "^1.7.1",
|
||||
"eslint-plugin-html": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-node": "^5.1.1",
|
||||
"eslint-plugin-promise": "^3.4.0",
|
||||
"eslint-loader": "^1.9.0",
|
||||
"eslint-plugin-html": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^5.2.1",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.14.1",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^0.11.1",
|
||||
"friendly-errors-webpack-plugin": "^1.1.3",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"http-proxy-middleware": "^0.17.3",
|
||||
"express": "^4.16.2",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.5",
|
||||
"friendly-errors-webpack-plugin": "^1.6.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"http-proxy-middleware": "^0.17.4",
|
||||
"opn": "^5.1.0",
|
||||
"optimize-css-assets-webpack-plugin": "^3.0.0",
|
||||
"ora": "^1.2.0",
|
||||
"rimraf": "^2.6.0",
|
||||
"semver": "^5.3.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
||||
"ora": "^1.3.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"semver": "^5.4.1",
|
||||
"shelljs": "^0.7.8",
|
||||
"sw-precache-webpack-plugin": "^0.11.4",
|
||||
"uglify-js": "^3.0.23",
|
||||
"url-loader": "^0.5.8",
|
||||
"vue-loader": "^13.0.2",
|
||||
"vue-style-loader": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.3",
|
||||
"webpack": "^3.4.1",
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
"webpack-dev-middleware": "^1.10.0",
|
||||
"webpack-hot-middleware": "^2.18.0",
|
||||
"webpack-merge": "^4.1.0",
|
||||
"uglify-js": "^3.1.10",
|
||||
"url-loader": "^0.6.2",
|
||||
"vue-loader": "^13.5.0",
|
||||
"vue-style-loader": "^3.0.3",
|
||||
"vue-template-compiler": "^2.5.8",
|
||||
"webpack": "^3.8.1",
|
||||
"webpack-bundle-analyzer": "^2.9.1",
|
||||
"webpack-dev-middleware": "^2.0.4",
|
||||
"webpack-hot-middleware": "^2.20.0",
|
||||
"webpack-merge": "^4.1.1",
|
||||
"yml-loader": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
25
publish.sh
Normal file
25
publish.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
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 "chore: 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 "chore: setting untracked version [ci skip]"
|
||||
git push
|
||||
|
||||
echo "Done!"
|
||||
@@ -1 +1 @@
|
||||
4b205fed13570c943ad67f9fc4db4f51aeb62cec
|
||||
2ba96e4a87cf7fcd056a9d72052623c5286ec7f1
|
||||
@@ -1,4 +1,4 @@
|
||||
package filemanager
|
||||
package staticgen
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
fm "github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/varutils"
|
||||
)
|
||||
|
||||
@@ -17,15 +18,6 @@ var (
|
||||
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
|
||||
)
|
||||
|
||||
// StaticGen is a static website generator.
|
||||
type StaticGen interface {
|
||||
SettingsPath() string
|
||||
|
||||
Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
||||
Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
||||
Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// Hugo is the Hugo static website generator.
|
||||
type Hugo struct {
|
||||
// Website root
|
||||
@@ -66,8 +58,13 @@ func (h Hugo) SettingsPath() string {
|
||||
return "/config." + frontmatter
|
||||
}
|
||||
|
||||
// Name is the plugin's name.
|
||||
func (h Hugo) Name() string {
|
||||
return "hugo"
|
||||
}
|
||||
|
||||
// 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
|
||||
// since we are only working with this method.
|
||||
if r.Method != http.MethodPost {
|
||||
@@ -87,7 +84,8 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
filename := filepath.Clean(r.URL.Path)
|
||||
filename = strings.TrimPrefix(filename, string(filepath.Separator))
|
||||
archetype := r.Header.Get("archetype")
|
||||
|
||||
ext := filepath.Ext(filename)
|
||||
@@ -110,8 +108,8 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
|
||||
}
|
||||
|
||||
// Publish publishes a post.
|
||||
func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
func (h Hugo) 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 strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
|
||||
@@ -127,7 +125,7 @@ func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// 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.
|
||||
if h.previewPath == "" {
|
||||
path, err := ioutil.TempDir("", "")
|
||||
@@ -186,7 +184,8 @@ func (h Hugo) undraft(file string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Hugo) find() error {
|
||||
// Setup sets up the plugin.
|
||||
func (h *Hugo) Setup() error {
|
||||
var err error
|
||||
if h.Exe, err = exec.LookPath("hugo"); err != nil {
|
||||
return err
|
||||
@@ -194,114 +193,3 @@ func (h *Hugo) find() error {
|
||||
|
||||
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