Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bda4fc16eb | ||
|
|
aa219e701e | ||
|
|
2460fd4fae | ||
|
|
d50bec8caa | ||
|
|
a5a68a8944 | ||
|
|
2029ca9aa6 | ||
|
|
311b943e40 | ||
|
|
620344bf0e | ||
|
|
7bb2d37661 | ||
|
|
e5f2331fb7 | ||
|
|
f158b79f89 | ||
|
|
de9cf466c7 | ||
|
|
b2f6f95916 | ||
|
|
ea2cc50a3f | ||
|
|
ef61957127 | ||
|
|
efe36312ec | ||
|
|
c5fbb47147 | ||
|
|
6602735030 | ||
|
|
eaafbb5dba | ||
|
|
33a99cf60c | ||
|
|
0a4e119bc5 | ||
|
|
16c8b86241 | ||
|
|
49c8dfa12e | ||
|
|
5c2fda6b4a | ||
|
|
1d3be7cf60 | ||
|
|
b8daa19d8f | ||
|
|
c6b392f1fb | ||
|
|
75e4afc1cb | ||
|
|
3c9762ee97 | ||
|
|
5968111f3e | ||
|
|
00be85db13 | ||
|
|
0d453229d9 | ||
|
|
4a4db4f4ee | ||
|
|
1755d52019 | ||
|
|
ea72ff6990 | ||
|
|
8777050d3e | ||
|
|
ae860e5bfc | ||
|
|
7ce9c72dc5 | ||
|
|
37282b0721 | ||
|
|
22fbc7d02d | ||
|
|
4905fc3800 | ||
|
|
242b868a56 | ||
|
|
e3c1e11b88 | ||
|
|
3cee6b67f0 | ||
|
|
7f46ef9a97 |
29
.goreleaser.yml
Normal file
29
.goreleaser.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
build:
|
||||
main: cmd/filemanager/main.go
|
||||
binary: filemanager
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- netbsd
|
||||
- openbsd
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
|
||||
archive:
|
||||
name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}"
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
19
.travis.yml
19
.travis.yml
@@ -1,14 +1,12 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- tip
|
||||
go: 1.8.3
|
||||
|
||||
env:
|
||||
- "PATH=/home/travis/gopath/bin:$PATH"
|
||||
|
||||
install:
|
||||
- go get ./...
|
||||
- go get github.com/mitchellh/gox
|
||||
# Install gometalinter and certain linters
|
||||
- go get github.com/alecthomas/gometalinter
|
||||
- go get github.com/client9/misspell/cmd/misspell
|
||||
@@ -20,16 +18,5 @@ script:
|
||||
- gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --exclude="rice-box.go" --tests ./...
|
||||
- go test ./... -timeout 30s
|
||||
|
||||
before_deploy:
|
||||
- cd cmd/filemanager
|
||||
- mkdir dist
|
||||
- gox -output "dist/{{.OS}}-{{.Arch}}-{{.Dir}}"
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: $GITHUB_TOKEN
|
||||
file_glob: true
|
||||
file: dist/*
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
after_success:
|
||||
- test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash
|
||||
|
||||
14
CONTRIBUTING.md
Normal file
14
CONTRIBUTING.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Contributing
|
||||
|
||||
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`
|
||||
2. `cd $GOPATH/src/github.com/hacdias/filemanager`
|
||||
3. `npm install`
|
||||
4. `npm run dev` - regenerates the static assets automatically
|
||||
5. `go install github.com/hacdias/filemanager/cmd/filemanager`
|
||||
6. Execute `$GOPATH/bin/filemanager`
|
||||
|
||||
The steps 3 and 4 are only required **if you want to develop the front-end**. Otherwise, you can ignore them. Before pulling, if you made any change on assets folder, you must run the `build.sh` script on the root of this repository.
|
||||
|
||||
If you are using this as a Caddy plugin, you should use its [official instructions for plugins](https://github.com/mholt/caddy/wiki/Extending-Caddy#2-plug-in-your-plugin) and import `github.com/hacdias/filemanager/caddy/filemanager`.
|
||||
75
README.md
75
README.md
@@ -11,8 +11,6 @@ filemanager provides a file managing interface within a specified directory and
|
||||
# Table of contents
|
||||
|
||||
+ [Getting started](#getting-started)
|
||||
- [Caddy](#caddy)
|
||||
- [Standalone](#standalone)
|
||||
+ [Features](#features)
|
||||
- [Users](#users)
|
||||
- [Search](#search)
|
||||
@@ -21,65 +19,7 @@ filemanager provides a file managing interface within a specified directory and
|
||||
|
||||
# Getting started
|
||||
|
||||
This is a library that can be used on your own applications as a middleware (see the [documentation](http://godoc.org/github.com/hacdias/filemanager)), as a plugin to Caddy web server or as a standalone app.
|
||||
|
||||
Once you have everything deployed, the default credentials to login to the filemanager are:
|
||||
|
||||
**Username:** `admin`
|
||||
**Password:** `admin`
|
||||
|
||||
## Caddy
|
||||
|
||||
The easiest way to get started is using this with Caddy web server. You just need to download Caddy from its [official website](https://caddyserver.com/download) with `http.filemanager` plugin enabled. For more information about the plugin itself, please refer to its [documentation](https://caddyserver.com/docs/http.filemanager).
|
||||
|
||||
## Standalone
|
||||
|
||||
You can use filemanager as a standalone executable. You just need to download it from the [releases page](https://github.com/hacdias/filemanager/releases), where you can find multiple releases.
|
||||
|
||||
You can either use flags or a JSON configuration file, which should have the following appearance:
|
||||
|
||||
```json
|
||||
{
|
||||
"port": 80,
|
||||
"address": "127.0.0.1",
|
||||
"database": "/path/to/database.db",
|
||||
"scope": "/path/to/my/files",
|
||||
"allowCommands": true,
|
||||
"allowEdit": true,
|
||||
"allowNew": true,
|
||||
"commands": [
|
||||
"git",
|
||||
"svn"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `scope`, `allowCommands`, `allowEdit`, `allowNew` and `commands` options are the defaults for new users. To set a configuration file, you will need to pass the path with a flag, like this: `filemanager --config=/path/to/config.json`.
|
||||
|
||||
Otherwise, you may not want to use a configuration file, which can be done using the following flags:
|
||||
|
||||
```
|
||||
-address string
|
||||
Address to listen to (default is all of them)
|
||||
-allow-commands
|
||||
Default allow commands option (default true)
|
||||
-allow-edit
|
||||
Default allow edit option (default true)
|
||||
-allow-new
|
||||
Default allow new option (default true)
|
||||
-commands string
|
||||
Space separated commands available for new users (default "git svn hg")
|
||||
-database string
|
||||
Database path (default "./filemanager.db")
|
||||
-port string
|
||||
HTTP Port (default is random)
|
||||
-scope string
|
||||
Default scope for new users (default ".")
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
(TODO)
|
||||
You can find the Getting Started guide on the [documentation](https://henriquedias.com/filemanager/quick-start/).
|
||||
|
||||
# Features
|
||||
|
||||
@@ -131,18 +71,7 @@ this are keywords case:insensitive
|
||||
|
||||
# Contributing
|
||||
|
||||
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`
|
||||
2. `cd $GOPATH/src/github.com/hacdias/filemanager`
|
||||
3. `npm install`
|
||||
4. `npm start dev` - regenerates the static assets automatically
|
||||
5. `go install gihthub.com/hacdias/filemanager/cmd/filemanager`
|
||||
6. Execute `$GOPATH/bin/filemanager`
|
||||
|
||||
The steps 3 and 4 are only required **if you want to develop the front-end**. Otherwise, you can ignore them. Before pulling, if you made any change on assets folder, you must run the `build.sh` script on the root of this repository.
|
||||
|
||||
If you are using this as a Caddy plugin, you should use its [official instructions for plugins](https://github.com/mholt/caddy/wiki/Extending-Caddy#2-plug-in-your-plugin) and import `github.com/hacdias/filemanager/caddy/filemanager`.
|
||||
The contributing guidelines can be found [here](https://github.com/hacdias/filemanager/blob/master/CONTRIBUTING.md).
|
||||
|
||||
# Donate
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ if (fs.existsSync('./rice-box.go')) {
|
||||
fs.unlinkSync('./rice-box.go')
|
||||
}
|
||||
|
||||
if (fs.existsSync('./caddy/hugo/rice-box.go')) {
|
||||
fs.unlinkSync('./caddy/hugo/rice-box.go')
|
||||
if (fs.existsSync('./plugins/rice-box.go')) {
|
||||
fs.unlinkSync('./plugins/rice-box.go')
|
||||
}
|
||||
|
||||
rm(path.join(config.assetsRoot, config.assetsSubDirectory), err => {
|
||||
|
||||
@@ -25,6 +25,10 @@ module.exports = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(yml|yaml)$/,
|
||||
loader: 'yml-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
|
||||
@@ -21,14 +21,13 @@
|
||||
<!-- 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.chunks) {
|
||||
<% for (var chunk of webpack.compilation.chunks) {
|
||||
for (var file of chunk.files) {
|
||||
if (file.match(/\.(js|css)$/)) { %>
|
||||
<link rel="<%= chunk.initial?'preload':'prefetch' %>" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
||||
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
||||
|
||||
<!-- Plugins info -->
|
||||
<script>{{ range $index, $plugin := .Plugins }}{{ JS $plugin.JavaScript }}{{ end}}</script>
|
||||
<script>{{ .JavaScript }}</script>
|
||||
<style>
|
||||
#loading {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<header>
|
||||
<div>
|
||||
<button @click="openSidebar" aria-label="Toggle sidebar" title="Toggle sidebar" class="action">
|
||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||
<i class="material-icons">menu</i>
|
||||
</button>
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<search></search>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="openSearch" aria-label="Search" title="Search" class="search-button action">
|
||||
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
||||
<i class="material-icons">search</i>
|
||||
</button>
|
||||
|
||||
<button v-show="showSaveButton" aria-label="Save" class="action" id="save-button">
|
||||
<i class="material-icons" title="Save">save</i>
|
||||
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
|
||||
<div v-for="plugin in plugins" :key="plugin.name">
|
||||
@@ -30,7 +30,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button @click="openMore" id="more" aria-label="More" title="More" class="action">
|
||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</button>
|
||||
|
||||
@@ -71,9 +71,9 @@
|
||||
<upload-button v-show="showUpload"></upload-button>
|
||||
<info-button v-show="showCommonButton"></info-button>
|
||||
|
||||
<button v-show="showSelectButton" @click="openSelect" aria-label="Select multiple" class="action">
|
||||
<button v-show="showSelectButton" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
|
||||
<i class="material-icons">check_circle</i>
|
||||
<span>Select</span>
|
||||
<span>{{ $t('buttons.select') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
@@ -92,7 +92,7 @@ import SwitchButton from './buttons/SwitchView'
|
||||
import MoveButton from './buttons/Move'
|
||||
import CopyButton from './buttons/Copy'
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
import * as api from '@/utils/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
|
||||
19
assets/src/components/Languages.vue
Normal file
19
assets/src/components/Languages.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="selected">
|
||||
<option value="en">{{ $t('languages.en') }}</option>
|
||||
<option value="pt">{{ $t('languages.pt') }}</option>
|
||||
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'languages',
|
||||
props: [ 'selected' ],
|
||||
methods: {
|
||||
change (event) {
|
||||
this.$emit('update:selected', event.target.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,118 +0,0 @@
|
||||
<template>
|
||||
<div id="login">
|
||||
<form @submit="submit">
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<h1>File Manager</h1>
|
||||
<div v-if="wrong" class="wrong">Wrong credentials</div>
|
||||
<input type="text" v-model="username" placeholder="Username">
|
||||
<input type="password" v-model="password" placeholder="Password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
data: function () {
|
||||
return {
|
||||
wrong: false,
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
let redirect = this.$route.query.redirect
|
||||
if (redirect === '' || redirect === undefined || redirect === null) {
|
||||
redirect = '/files/'
|
||||
}
|
||||
|
||||
auth.login(this.username, this.password)
|
||||
.then(() => {
|
||||
this.$router.push({ path: redirect })
|
||||
})
|
||||
.catch(() => {
|
||||
this.wrong = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#login {
|
||||
background: #fff;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#login img {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#login h1 {
|
||||
text-align: center;
|
||||
font-size: 2.5em;
|
||||
margin: .4em 0 .67em;
|
||||
}
|
||||
|
||||
#login form {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 16em;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#login input {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
#login .wrong {
|
||||
background: #F44336;
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
}
|
||||
|
||||
@keyframes opac {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#login input[type="text"],
|
||||
#login input[type="password"] {
|
||||
padding: .5em 1em;
|
||||
border: 1px solid #e9e9e9;
|
||||
transition: .2s ease border;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#login input[type="text"]:focus,
|
||||
#login input[type="password"]:focus,
|
||||
#login input[type="text"]:hover,
|
||||
#login input[type="password"]:hover {
|
||||
border-color: #9f9f9f;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>Profile Settings</h1>
|
||||
|
||||
<ul v-if="user.admin">
|
||||
<li><router-link to="/settings/global">Go to Global Settings</router-link></li>
|
||||
</ul>
|
||||
|
||||
<form @submit="changePassword">
|
||||
<h2>Change Password</h2>
|
||||
<p><input :class="passwordClass" type="password" placeholder="Your new password" v-model="password" name="password"></p>
|
||||
<p><input :class="passwordClass" type="password" placeholder="Confirm your new password" v-model="passwordConf" name="password"></p>
|
||||
<p><input type="submit" value="Change Password"></p>
|
||||
</form>
|
||||
|
||||
<form @submit="updateCSS">
|
||||
<h2>Custom Stylesheet</h2>
|
||||
<textarea v-model="css" name="css"></textarea>
|
||||
<p><input type="submit" value="Update"></p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
data: function () {
|
||||
return {
|
||||
password: '',
|
||||
passwordConf: '',
|
||||
css: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ]),
|
||||
passwordClass () {
|
||||
if (this.password === '' && this.passwordConf === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (this.password === this.passwordConf) {
|
||||
return 'green'
|
||||
}
|
||||
|
||||
return 'red'
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.css = this.user.css
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showSuccess' ]),
|
||||
changePassword (event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (this.password !== this.passwordConf) {
|
||||
return
|
||||
}
|
||||
|
||||
api.updatePassword(this.password).then(() => {
|
||||
this.showSuccess('Password updated!')
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
},
|
||||
updateCSS (event) {
|
||||
event.preventDefault()
|
||||
|
||||
api.updateCSS(this.css).then(() => {
|
||||
this.$store.commit('setUserCSS', this.css)
|
||||
this.$emit('css-updated')
|
||||
this.showSuccess('Styles updated!')
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
|
||||
<div id="input">
|
||||
<button v-if="active" class="action" @click="close">
|
||||
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</button>
|
||||
<i v-else class="material-icons">search</i>
|
||||
@@ -11,7 +11,7 @@
|
||||
ref="input"
|
||||
:autofocus="active"
|
||||
v-model.trim="value"
|
||||
aria-label="Write here to search"
|
||||
:aria-label="$t('search.writeToSearch')"
|
||||
:placeholder="placeholder">
|
||||
</div>
|
||||
|
||||
@@ -78,10 +78,10 @@ export default {
|
||||
// Placeholder value.
|
||||
placeholder: function () {
|
||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||
return 'Search or execute a command...'
|
||||
return this.$t('search.searchOrCommand')
|
||||
}
|
||||
|
||||
return 'Search...'
|
||||
return this.$t('search.search')
|
||||
},
|
||||
// The text that is shown on the results' box while
|
||||
// there is no search result or command output to show.
|
||||
@@ -92,16 +92,16 @@ export default {
|
||||
|
||||
if (this.value.length === 0) {
|
||||
if (this.user.allowCommands && this.user.commands.length > 0) {
|
||||
return `Search or use one of your supported commands: ${this.user.commands.join(', ')}.`
|
||||
return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.`
|
||||
}
|
||||
|
||||
return 'Type and press enter to search.'
|
||||
this.$t('search.type')
|
||||
}
|
||||
|
||||
if (!this.supported() || !this.user.allowCommands) {
|
||||
return 'Press enter to search.'
|
||||
return this.$t('search.pressToSearch')
|
||||
} else {
|
||||
return 'Press enter to execute.'
|
||||
return this.$t('search.pressToExecute')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<nav :class="{active}">
|
||||
<router-link class="action" to="/files/" aria-label="My Files" title="My Files">
|
||||
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
|
||||
<i class="material-icons">folder</i>
|
||||
<span>My Files</span>
|
||||
<span>{{ $t('sidebar.myFiles') }}</span>
|
||||
</router-link>
|
||||
|
||||
<div v-if="user.allowNew">
|
||||
<button @click="$store.commit('showHover', 'newDir')" aria-label="New directory" title="New directory" class="action">
|
||||
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
|
||||
<i class="material-icons">create_new_folder</i>
|
||||
<span>New folder</span>
|
||||
<span>{{ $t('sidebar.newFolder') }}</span>
|
||||
</button>
|
||||
|
||||
<button @click="$store.commit('showHover', 'newFile')" aria-label="New file" title="New file" class="action">
|
||||
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
|
||||
<i class="material-icons">note_add</i>
|
||||
<span>New file</span>
|
||||
<span>{{ $t('sidebar.newFile') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -24,22 +24,22 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<router-link class="action" to="/settings" aria-label="Settings" title="Settings">
|
||||
<div v-if="!$store.state.user.noAuth">
|
||||
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
||||
<i class="material-icons">settings_applications</i>
|
||||
<span>Settings</span>
|
||||
<span>{{ $t('sidebar.settings') }}</span>
|
||||
</router-link>
|
||||
|
||||
<button @click="logout" class="action" id="logout" aria-label="Log out" title="Logout">
|
||||
<button @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>Logout</span>
|
||||
<span>{{ $t('sidebar.logout') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="credits">
|
||||
<span>Served with <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
|
||||
<span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span>
|
||||
<span v-for="plugin in plugins" :key="plugin.name" v-html="plugin.credits"><br></span>
|
||||
<span><a @click="help">Help</a></span>
|
||||
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
|
||||
</p>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Copy" title="Copy" class="action" id="copy-button">
|
||||
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
|
||||
<i class="material-icons">content_copy</i>
|
||||
<span>Copy file</span>
|
||||
<span>{{ $t('buttons.copyFile') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Delete" title="Delete" class="action" id="delete-button">
|
||||
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
|
||||
<i class="material-icons">delete</i>
|
||||
<span>Delete</span>
|
||||
<span>{{ $t('buttons.delete') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="download" aria-label="Download" title="Download" id="download-button" class="action">
|
||||
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
|
||||
<i class="material-icons">file_download</i>
|
||||
<span>Download</span>
|
||||
<span>{{ $t('buttons.download') }}</span>
|
||||
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button title="Info" aria-label="Info" class="action" @click="show">
|
||||
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
|
||||
<i class="material-icons">info</i>
|
||||
<span>Info</span>
|
||||
<span>{{ $t('buttons.info') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Move" title="Move" class="action" id="move-button">
|
||||
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
|
||||
<i class="material-icons">forward</i>
|
||||
<span>Move file</span>
|
||||
<span>{{ $t('buttons.moveFile') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="show" aria-label="Rename" title="Rename" class="action" id="rename-button">
|
||||
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
|
||||
<i class="material-icons">mode_edit</i>
|
||||
<span>Rename</span>
|
||||
<span>{{ $t('buttons.rename') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="change" aria-label="Switch View" title="Switch View" class="action" id="switch-view-button">
|
||||
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
|
||||
<i class="material-icons">{{ icon() }}</i>
|
||||
<span>Switch view</span>
|
||||
<span>{{ $t('buttons.switchView') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button @click="upload" aria-label="Upload" title="Upload" class="action" id="upload-button">
|
||||
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
|
||||
<i class="material-icons">file_upload</i>
|
||||
<span>Upload</span>
|
||||
<span>{{ $t('buttons.upload') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<form id="editor" :class="req.language">
|
||||
<div v-if="hasMetadata" id="metadata">
|
||||
<h2>Metadata</h2>
|
||||
<h2>{{ $t('files.metadata') }}</h2>
|
||||
</div>
|
||||
|
||||
<h2 v-if="hasMetadata">Body</h2>
|
||||
<h2 v-if="hasMetadata">{{ $t('files.body') }}</h2>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -123,7 +123,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -2,9 +2,9 @@
|
||||
<div v-if="(req.numDirs + req.numFiles) == 0">
|
||||
<h2 class="message">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>It feels lonely here...</span>
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
</h2>
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="req.display"
|
||||
@@ -16,21 +16,23 @@
|
||||
<div></div>
|
||||
<div>
|
||||
<p :class="{ active: nameSorted }" class="name" @click="sort('name')">
|
||||
<span>Name</span>
|
||||
<span>{{ $t('files.name') }}</span>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p :class="{ active: !nameSorted }" class="size" @click="sort('size')">
|
||||
<span>Size</span>
|
||||
<p :class="{ active: sizeSorted }" class="size" @click="sort('size')">
|
||||
<span>{{ $t('files.size') }}</span>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p class="modified">Last modified</p>
|
||||
<p :class="{ active: modifiedSorted }" class="modified" @click="sort('modified')">
|
||||
<span>{{ $t('files.lastModified') }}</span>
|
||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numDirs > 0">Folders</h2>
|
||||
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
|
||||
<div v-if="req.numDirs > 0">
|
||||
<item v-for="(item, index) in req.items"
|
||||
v-if="item.isDir"
|
||||
@@ -45,7 +47,7 @@
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<h2 v-if="req.numFiles > 0">Files</h2>
|
||||
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
|
||||
<div v-if="req.numFiles > 0">
|
||||
<item v-for="(item, index) in req.items"
|
||||
v-if="!item.isDir"
|
||||
@@ -60,12 +62,12 @@
|
||||
</item>
|
||||
</div>
|
||||
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" value="Upload" multiple>
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
|
||||
<div v-show="$store.state.multiple" :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||
<p>Multiple selection enabled</p>
|
||||
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" title="Clear" aria-label="Clear" class="action">
|
||||
<i class="material-icons" title="Clear">clear</i>
|
||||
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
|
||||
<i class="material-icons">clear</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,6 +88,12 @@ export default {
|
||||
nameSorted () {
|
||||
return (this.req.sort === 'name')
|
||||
},
|
||||
sizeSorted () {
|
||||
return (this.req.sort === 'size')
|
||||
},
|
||||
modifiedSorted () {
|
||||
return (this.req.sort === 'modified')
|
||||
},
|
||||
ascOrdered () {
|
||||
return (this.req.order === 'asc')
|
||||
},
|
||||
@@ -97,7 +105,14 @@ export default {
|
||||
return 'arrow_downward'
|
||||
},
|
||||
sizeIcon () {
|
||||
if (!this.nameSorted && this.ascOrdered) {
|
||||
if (this.sizeSorted && this.ascOrdered) {
|
||||
return 'arrow_downward'
|
||||
}
|
||||
|
||||
return 'arrow_upward'
|
||||
},
|
||||
modifiedIcon () {
|
||||
if (this.modifiedSorted && this.ascOrdered) {
|
||||
return 'arrow_downward'
|
||||
}
|
||||
|
||||
@@ -275,10 +290,14 @@ export default {
|
||||
if (this.nameIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
} else {
|
||||
} else if (sort === 'size') {
|
||||
if (this.sizeIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
} else if (sort === 'modified') {
|
||||
if (this.modifiedIcon === 'arrow_upward') {
|
||||
order = 'asc'
|
||||
}
|
||||
}
|
||||
|
||||
let path = this.$store.state.baseURL
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="previewer">
|
||||
<div class="bar">
|
||||
<button @click="back" class="action" aria-label="Close Preview" id="close">
|
||||
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
@@ -11,8 +11,12 @@
|
||||
<info-button></info-button>
|
||||
</div>
|
||||
|
||||
<button class="action" @click="prev" v-show="hasPrevious"><i class="material-icons">chevron_left</i></button>
|
||||
<button class="action" @click="next" v-show="hasNext"><i class="material-icons">chevron_right</i></button>
|
||||
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
||||
<i class="material-icons">chevron_left</i>
|
||||
</button>
|
||||
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</button>
|
||||
|
||||
<div class="preview">
|
||||
<img v-if="req.type == 'image'" :src="raw()">
|
||||
@@ -24,7 +28,7 @@
|
||||
</video>
|
||||
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw()"></object>
|
||||
<a v-else-if="req.type == 'blob'" :href="download()">
|
||||
<h2 class="message">Download <i class="material-icons">file_download</i></h2>
|
||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
<pre v-else >{{ req.content }}</pre>
|
||||
</div>
|
||||
@@ -35,10 +39,10 @@
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import api from '@/utils/api'
|
||||
import InfoButton from './buttons/Info'
|
||||
import DeleteButton from './buttons/Delete'
|
||||
import RenameButton from './buttons/Rename'
|
||||
import DownloadButton from './buttons/Download'
|
||||
import InfoButton from '@/components/buttons/Info'
|
||||
import DeleteButton from '@/components/buttons/Delete'
|
||||
import RenameButton from '@/components/buttons/Rename'
|
||||
import DownloadButton from '@/components/buttons/Download'
|
||||
|
||||
export default {
|
||||
name: 'preview',
|
||||
@@ -1,13 +1,16 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Copy</h3>
|
||||
<p>Choose the place to copy your files:</p>
|
||||
<h3>{{ $t('prompts.copy') }}</h3>
|
||||
<p>{{ $t('prompts.copyMessage') }}</p>
|
||||
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
|
||||
<div>
|
||||
<button class="ok" @click="copy">Copy</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
<button class="ok" @click="copy">{{ $t('buttons.copy') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Delete files</h3>
|
||||
<p v-show="req.kind !== 'listing'">Are you sure you want to delete this file/folder?</p>
|
||||
<p v-show="req.kind === 'listing'">Are you sure you want to delete {{ selectedCount }} file(s)?</p>
|
||||
<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" autofocus>Delete</button>
|
||||
<button @click="closeHovers" class="cancel">Cancel</button>
|
||||
<button @click="submit" autofocus>{{ $t('buttons.delete') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="prompt" id="download">
|
||||
<h3>Download files</h3>
|
||||
<p>Choose the format you want to download.</p>
|
||||
<h3>{{ $t('prompts.download') }}</h3>
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="prompt error">
|
||||
<i class="material-icons">error_outline</i>
|
||||
<h3>Something went wrong</h3>
|
||||
<h3>{{ $t('prompts.error') }}</h3>
|
||||
<pre>{{ $store.state.showMessage }}</pre>
|
||||
<div>
|
||||
<button @click="close" autofocus>Close</button>
|
||||
<button @click="reportIssue" class="cancel">Report Issue</button>
|
||||
<button @click="close" autofocus>{{ $t('buttons.close') }}</button>
|
||||
<button @click="reportIssue" class="cancel">{{ $t('buttons.reportIssue') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:data-url="item.url">{{ item.name }}</li>
|
||||
</ul>
|
||||
|
||||
<p>Currently navigating on: <code>{{ nav }}</code>.</p>
|
||||
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
<template>
|
||||
<div class="prompt help">
|
||||
<h3>Help</h3>
|
||||
<h3>{{ $t('help.help') }}</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>F1</strong> - this information</li>
|
||||
<li><strong>F2</strong> - rename file</li>
|
||||
<li><strong>DEL</strong> - delete selected items</li>
|
||||
<li><strong>ESC</strong> - clear selection and/or close the prompt</li>
|
||||
<li><strong>CTRL + S</strong> - save a file or download the directory where you are</li>
|
||||
<li><strong>CTRL + Click</strong> - select multiple files or directories</li>
|
||||
<li><strong>Double click</strong> - open a file or directory</li>
|
||||
<li><strong>Click</strong> - select file or directory</li>
|
||||
</ul>
|
||||
|
||||
<p>Not available yet</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Alt + Click</strong> - select a group of files</li>
|
||||
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
|
||||
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
|
||||
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
|
||||
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
|
||||
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
|
||||
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
|
||||
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
|
||||
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
|
||||
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
|
||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>File Information</h3>
|
||||
<h3>{{ $t('prompts.fileInfo') }}</h3>
|
||||
|
||||
<p v-show="selected.length > 1">{{ selected.length }} files selected.</p>
|
||||
<p v-show="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
||||
|
||||
<p v-show="selected.length < 2"><strong>Display Name:</strong> {{ name() }}</p>
|
||||
<p><strong>Size:</strong> <span id="content_length"></span>{{ humanSize() }}</p>
|
||||
<p v-show="selected.length < 2"><strong>Last Modified:</strong> {{ humanTime() }}</p>
|
||||
<p v-show="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>
|
||||
|
||||
<section v-show="dir() && selected.length === 0">
|
||||
<p><strong>Number of files:</strong> {{ req.numFiles }}</p>
|
||||
<p><strong>Number of directories:</strong> {{ req.numDirs }}</p>
|
||||
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
|
||||
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
|
||||
</section>
|
||||
|
||||
<section v-show="!dir()">
|
||||
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">show</a></code></p>
|
||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">show</a></code></p>
|
||||
<p><strong>SHA256:</strong> <code><a @click="checksum($event, 'sha256')">show</a></code></p>
|
||||
<p><strong>SHA512:</strong> <code><a @click="checksum($event, 'sha512')">show</a></code></p>
|
||||
<p><strong>MD5:</strong> <code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>SHA1:</strong> <code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
|
||||
<p><strong>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>
|
||||
|
||||
<div>
|
||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">OK</button>
|
||||
<button type="submit" @click="$store.commit('closeHovers')" class="ok">{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Move</h3>
|
||||
<p>Choose new house for your file(s)/folder(s):</p>
|
||||
<h3>{{ $t('prompts.move') }}</h3>
|
||||
<p>{{ $t('prompts.moveMessage') }}</p>
|
||||
|
||||
<file-list @update:selected="val => dest = val"></file-list>
|
||||
|
||||
<div>
|
||||
<button class="ok" @click="move">Move</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
<button class="ok" @click="move">{{ $t('buttons.move') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>New directory</h3>
|
||||
<p>Write the name of the new directory.</p>
|
||||
<h3>{{ $t('prompts.newDir') }}</h3>
|
||||
<p>{{ $t('prompts.newDirMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok" @click="submit">Create</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
<button class="ok" @click="submit">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>New file</h3>
|
||||
<p>Write the name of the new file.</p>
|
||||
<h3>{{ $t('prompts.newFile') }}</h3>
|
||||
<p>{{ $t('prompts.newFileMessage') }}</p>
|
||||
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
|
||||
<div>
|
||||
<button class="ok" @click="submit">Create</button>
|
||||
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||
<button class="ok" @click="submit">{{ $t('buttons.create') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,7 +27,10 @@
|
||||
:placeholder="input.placeholder">
|
||||
<div>
|
||||
<input type="submit" class="ok" :value="prompt.ok">
|
||||
<button class="cancel" @click.prevent="$store.commit('closeHovers')">Cancel</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<h3>Rename</h3>
|
||||
<p>Insert a new name for <code>{{ oldName() }}</code>:</p>
|
||||
<h3>{{ $t('prompts.rename') }}</h3>
|
||||
<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">Rename</button>
|
||||
<button @click="cancel" class="cancel">Cancel</button>
|
||||
<button @click="submit" type="submit">{{ $t('buttons.rename') }}</button>
|
||||
<button class="cancel"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i class="material-icons">done</i>
|
||||
<h3>{{ $store.state.showMessage }}</h3>
|
||||
<div>
|
||||
<button @click="close" autofocus>OK</button>
|
||||
<button @click="close" autofocus>{{ $t('buttons.ok') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -133,5 +133,15 @@ main {
|
||||
}
|
||||
|
||||
#breadcrumbs a {
|
||||
color: inherit
|
||||
color: inherit;
|
||||
transition: .1s ease-in;
|
||||
border-radius: .125em;
|
||||
}
|
||||
|
||||
#breadcrumbs a:hover {
|
||||
background-color: rgba(0,0,0, 0.05);
|
||||
}
|
||||
|
||||
#breadcrumbs span a {
|
||||
padding: .2em;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
width: 1em
|
||||
}
|
||||
|
||||
.dashboard > *:first-child {
|
||||
.dashboard > h1:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ form.dashboard > p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dashboard select,
|
||||
.dashboard textarea,
|
||||
.dashboard input[type="text"],
|
||||
.dashboard input[type="password"] {
|
||||
@@ -60,12 +61,18 @@ form.dashboard > p:last-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard #locale,
|
||||
.dashboard #username,
|
||||
.dashboard #password,
|
||||
.dashboard #scope {
|
||||
max-width: 18em;
|
||||
}
|
||||
|
||||
.dashboard #locale {
|
||||
border: 1px solid #dddddd;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
.dashboard textarea:focus,
|
||||
.dashboard textarea:hover,
|
||||
.dashboard input[type="text"]:focus,
|
||||
@@ -118,3 +125,27 @@ p code {
|
||||
font-size: .8em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dashboard #nav {
|
||||
list-style: none;
|
||||
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);
|
||||
}
|
||||
|
||||
.dashboard #nav li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard #nav li:last-child {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.dashboard #nav i {
|
||||
font-size: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
68
assets/src/css/login.css
Normal file
68
assets/src/css/login.css
Normal file
@@ -0,0 +1,68 @@
|
||||
#login {
|
||||
background: #fff;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#login img {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#login h1 {
|
||||
text-align: center;
|
||||
font-size: 2.5em;
|
||||
margin: .4em 0 .67em;
|
||||
}
|
||||
|
||||
#login form {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 16em;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#login input {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
#login .wrong {
|
||||
background: #F44336;
|
||||
color: #fff;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
animation: .2s opac forwards;
|
||||
}
|
||||
|
||||
@keyframes opac {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#login input[type="text"],
|
||||
#login input[type="password"] {
|
||||
padding: .5em 1em;
|
||||
border: 1px solid #e9e9e9;
|
||||
transition: .2s ease border;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#login input[type="text"]:focus,
|
||||
#login input[type="password"]:focus,
|
||||
#login input[type="text"]:hover,
|
||||
#login input[type="password"]:hover {
|
||||
border-color: #9f9f9f;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
@import "./listing.css";
|
||||
@import "./editor.css";
|
||||
@import "./dashboard.css";
|
||||
@import "./login.css";
|
||||
|
||||
/* * * * * * * * * * * * * * * *
|
||||
* ACTION *
|
||||
|
||||
164
assets/src/i18n/en.yaml
Normal file
164
assets/src/i18n/en.yaml
Normal file
@@ -0,0 +1,164 @@
|
||||
buttons:
|
||||
cancel: Cancel
|
||||
close: Close
|
||||
copy: Copy
|
||||
copyFile: Copy file
|
||||
create: Create
|
||||
delete: Delete
|
||||
download: Download
|
||||
info: Info
|
||||
more: More
|
||||
move: Move
|
||||
moveFile: Move file
|
||||
new: New
|
||||
next: Next
|
||||
ok: OK
|
||||
previous: Previous
|
||||
rename: Rename
|
||||
reportIssue: Report Issue
|
||||
save: Save
|
||||
search: Search
|
||||
select: Select
|
||||
selectMultiple: Select multiple
|
||||
switchView: Swicth view
|
||||
toggleSidebar: Toggle sidebar
|
||||
update: Update
|
||||
upload: Upload
|
||||
errors:
|
||||
forbidden: You're not welcome here.
|
||||
internal: Something really went wrong.
|
||||
notFound: This location can't be reached.
|
||||
files:
|
||||
folders: Folders
|
||||
files: Files
|
||||
body: Body
|
||||
clear: Clear
|
||||
closePreview: Close preview
|
||||
home: Home
|
||||
lastModified: Last modified
|
||||
loading: Loading...
|
||||
lonely: It feels lonely here...
|
||||
metadata: Metadata
|
||||
multipleSelectionEnabled: Multiple selection enabled
|
||||
name: Name
|
||||
size: Size
|
||||
help:
|
||||
click: select file or directory
|
||||
ctrl:
|
||||
click: select multiple files or directories
|
||||
f: opens search
|
||||
s: save a file or download the directory where you are
|
||||
del: delete selected items
|
||||
doubleClick: open a file or directory
|
||||
esc: clear selection and/or close the prompt
|
||||
f1: this information
|
||||
f2: rename file
|
||||
help: Help
|
||||
login:
|
||||
password: Password
|
||||
submit: Login
|
||||
username: Username
|
||||
wrongCredentials: Wrong credentials
|
||||
prompts:
|
||||
copy: Copy
|
||||
copyMessage: 'Choose the place to copy your files:'
|
||||
currentlyNavigating: 'Currently navigating on:'
|
||||
deleteMessageMultiple: Are you sure you want to delete {count} file(s)?
|
||||
deleteMessageSingle: Are you sure you want to delete this file/folder?
|
||||
deleteTitle: Delete files
|
||||
displayName: 'Display Name:'
|
||||
download: Download files
|
||||
downloadMessage: Choose the format you want to download.
|
||||
error: Something went wrong
|
||||
fileInfo: File information
|
||||
filesSelected: "{count} files selected."
|
||||
lastModified: Last Modified
|
||||
move: Move
|
||||
moveMessage: 'Choose new house for your file(s)/folder(s):'
|
||||
newDir: New directory
|
||||
newDirMessage: Write the name of the new directory.
|
||||
newFile: New file
|
||||
newFileMessage: Write the name of the new file.
|
||||
numberDirs: Number of directories
|
||||
numberFiles: Number of files
|
||||
rename: Rename
|
||||
renameMessage: Insert a new name for
|
||||
show: Show
|
||||
size: Size
|
||||
settings:
|
||||
admin: Admin
|
||||
administrator: Administrator
|
||||
allowCommands: Execute commands
|
||||
allowEdit: Edit, rename and delete files or directories.
|
||||
allowNew: Create new files and directories
|
||||
avoidChanges: "(leave blank to avoid changes)"
|
||||
changePassword: Change Password
|
||||
commands: Commands
|
||||
commandsHelp: >
|
||||
Here you can set commands that are executed in the named events. You
|
||||
write one command per line. If the event is related to files, such as before and
|
||||
after saving, the environment variable "file" will be available with the path
|
||||
of the file.
|
||||
commandsUpdated: Commands updated!
|
||||
customStylesheet: Custom Stylesheet
|
||||
examples: Examples
|
||||
globalSettings: Global Settings
|
||||
language: Language
|
||||
newPassword: Your new password
|
||||
newPasswordConfirm: Confirm your new password
|
||||
newUser: New User
|
||||
password: Password
|
||||
passwordUpdated: Password updated!
|
||||
permissions: Permissions
|
||||
permissionsHelp: >
|
||||
You can set the user to be an administrator or choose the permissions
|
||||
individually. If you select "Administrator", all of the other options will be
|
||||
automatically checked. The management of users remains a privilege of an administrator.
|
||||
pluginsUpdated: Plugins settings updated!
|
||||
profileSettings: Profile Settings
|
||||
ruleExample1: >
|
||||
'prevents the access to any dot file (such as .git, .gitignore) in
|
||||
every folder.'
|
||||
ruleExample2: blocks the access to the file named Caddyfile on the root of the scope.
|
||||
rules: Rules
|
||||
rulesHelp1: >
|
||||
'Here you can define a set of allow and disallow rules for this specific
|
||||
user. The blocked files won''t show up in the listings and they won''t be accessible
|
||||
to the user. We support regex and paths relative to the user''s scope.'
|
||||
rulesHelp2: >
|
||||
Each rule goes in one different line and must start with the keyword
|
||||
{0} or {1}. Then you should write {2} if you are using a regular expression and
|
||||
then the expression or the path.
|
||||
scope: Scope
|
||||
settingsUpdated: Settings updated!
|
||||
user: User
|
||||
userCommands: Commands
|
||||
userCommandsHelp:
|
||||
'A space separated list with the available commands for this user.
|
||||
Example:'
|
||||
userCreated: User created!
|
||||
userDeleted: User deleted!
|
||||
userManagement: User Management
|
||||
username: Username
|
||||
users: Users
|
||||
userUpdated: User updated!
|
||||
sidebar:
|
||||
help: Help
|
||||
logout: Logout
|
||||
myFiles: My files
|
||||
newFile: New file
|
||||
newFolder: New folder
|
||||
servedWith: Served with
|
||||
settings: Settings
|
||||
search:
|
||||
writeToSearch: Write here to search
|
||||
searchOrCommand: Search or execute a command...
|
||||
searchOrSupportedCommand: 'Search or use one of your supported commands:'
|
||||
search: Search...
|
||||
type: Type and press enter to search.
|
||||
pressToSearch: Press enter to search.
|
||||
pressToExecute: Press enter to execute.
|
||||
languages:
|
||||
en: English
|
||||
pt: Portuguese
|
||||
zhCN: Chinese (Simplified)
|
||||
19
assets/src/i18n/index.js
Normal file
19
assets/src/i18n/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import en from './en.yaml'
|
||||
import pt from './pt.yaml'
|
||||
import zhCN from './zh-cn.yaml'
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
'en': en,
|
||||
'pt': pt,
|
||||
'zh-cn': zhCN
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
165
assets/src/i18n/pt.yaml
Normal file
165
assets/src/i18n/pt.yaml
Normal file
@@ -0,0 +1,165 @@
|
||||
buttons:
|
||||
cancel: Cancelar
|
||||
close: Fechar
|
||||
copy: Copiar
|
||||
copyFile: Copiar ficheiro
|
||||
create: Criar
|
||||
delete: Eliminar
|
||||
download: Descarregar
|
||||
info: Info
|
||||
more: Mais
|
||||
move: Mover
|
||||
moveFile: Mover ficheiro
|
||||
new: Novo
|
||||
next: Próximo
|
||||
ok: OK
|
||||
previous: Anterior
|
||||
rename: Renomear
|
||||
reportIssue: Reportar Erro
|
||||
save: Guardar
|
||||
search: Pesquisar
|
||||
select: Selecionar
|
||||
selectMultiple: Selecionar múltiplos
|
||||
switchView: Alterar modo de visão
|
||||
toggleSidebar: Alternar barra lateral
|
||||
update: Atualizar
|
||||
upload: Enviar
|
||||
errors:
|
||||
forbidden: Tu não és bem-vindo aqui.
|
||||
internal: Algo correu bastante mal.
|
||||
notFound: Não conseguimos chegar a esta localização.
|
||||
files:
|
||||
folders: Pastas
|
||||
files: Ficheiros
|
||||
body: Corpo
|
||||
clear: Limpar
|
||||
closePreview: Fechar pré-visualização
|
||||
home: Início
|
||||
lastModified: Última modificação
|
||||
loading: A carregar...
|
||||
lonely: Sinto-me sozinho...
|
||||
metadata: Metadados
|
||||
multipleSelectionEnabled: Seleção múltipla ativada
|
||||
name: Nome
|
||||
size: Tamanho
|
||||
help:
|
||||
click: selecionar pasta ou ficheiro
|
||||
ctrl:
|
||||
click: selecionar várias pastas e ficheiros
|
||||
f: pesquisar
|
||||
s: guardar um ficheiro ou descarregar a pasta em que estás a navegar
|
||||
del: eliminar os ficheiros selecionados
|
||||
doubleClick: abrir pasta ou ficheiro
|
||||
esc: limpar seleção e/ou fechar menu
|
||||
f1: esta informação
|
||||
f2: renomear ficheiro
|
||||
help: Ajuda
|
||||
login:
|
||||
password: Palavra-passe
|
||||
submit: Login
|
||||
username: Nome de utilizador
|
||||
wrongCredentials: Dados errados
|
||||
prompts:
|
||||
copy: Copiar
|
||||
copyMessage: 'Escolhe um lugar para copiar os ficheiros:'
|
||||
currentlyNavigating: 'A navegar em:'
|
||||
deleteMessageMultiple: Deseja eliminar {count} ficheiro(s)?
|
||||
deleteMessageSingle: Deseja eliminar esta pasta/ficheiro?
|
||||
deleteTitle: Eliminar ficheiros
|
||||
displayName: 'Nome:'
|
||||
download: Descarregar ficheiros
|
||||
downloadMessage: Escolha o formato do ficheiro.
|
||||
error: Algo correu mal
|
||||
fileInfo: Informação do ficheiro
|
||||
filesSelected: "{count} ficheiros selecionados."
|
||||
lastModified: Última Modificação
|
||||
move: Mover
|
||||
moveMessage: 'Escolha uma nova casa para os seus ficheiros:'
|
||||
newDir: Nova pasta
|
||||
newDirMessage: Escreva o nome da nova pasta.
|
||||
newFile: Novo ficheiro
|
||||
newFileMessage: Escreva o nome do novo ficheiro.
|
||||
numberDirs: Número de pastas
|
||||
numberFiles: Número de ficheiros
|
||||
rename: Renomear
|
||||
renameMessage: Insira um novo nome para
|
||||
show: Mostrar
|
||||
size: Tamanho
|
||||
settings:
|
||||
admin: Admin
|
||||
administrator: Administrador
|
||||
allowCommands: Executar comandos
|
||||
allowEdit: Editar, renomear e eliminar ficheiros ou pastas
|
||||
allowNew: Criar novos ficheiros e pastas
|
||||
avoidChanges: "(deixe em branco para manter)"
|
||||
changePassword: Alterar Password
|
||||
commands: Comandos
|
||||
commandsHelp: >
|
||||
Pode definir um conjunto de comandos a executar em determiandos eventos. Deve
|
||||
escrever um comando por linha. Se o evento estiver relacionado com ficheiros,
|
||||
como antes e depois de guardar, irá existir uma variável de ambiente denominada
|
||||
"file" com o caminho do ficheiro.
|
||||
commandsUpdated: Comandos atualizados!
|
||||
customStylesheet: Estilos Personalizados
|
||||
examples: Exemplos
|
||||
globalSettings: Configurações Globais
|
||||
language: Linguagem
|
||||
newPassword: Nova palavra-passe
|
||||
newPasswordConfirm: Confirme a nova palavra-passe
|
||||
newUser: Novo Utilizador
|
||||
password: Palavra-passe
|
||||
passwordUpdated: Palavra-passe atualizada!
|
||||
permissions: Permissões
|
||||
permissionsHelp: >
|
||||
Pode definir o utilizador como administrador ou escolher as permissões manualmente.
|
||||
Se selecionar a opção "Administrador", todas as outras opções serão automaticamente
|
||||
selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores.
|
||||
pluginsUpdated: Plugins atualizados!
|
||||
profileSettings: Configurações do Utilizador
|
||||
ruleExample1: >
|
||||
previne o acesso a qualquer "dotfile" (como .git, .gitignore) em qualquer pasta
|
||||
ruleExample2: bloqueia o acesso ao ficheiro chamado Caddyfile.
|
||||
rules: Regras
|
||||
rulesHelp1: >
|
||||
Aqui pode definir um conjunto de regras para permitir ou bloquear o acesso
|
||||
do utilizador a determinados ficheiros ou pastas. Os ficheiros bloqueados não
|
||||
irão aparecer durante a navegação. Suportamos expressões regulares e os caminhos
|
||||
dos ficheiros devem ser relativos à base do utilizador.
|
||||
rulesHelp2: >
|
||||
Cada regra deve ser colocada numa linha diferente e deve começar com as
|
||||
palavras {0} (permite) ou {1} (bloqueia). Deve escrever, logo de seguida, {2},
|
||||
caso queira utilizar uma expressão regular. Depois, escreva o caminho do ficheiro/pasta
|
||||
ou a expressão regular.
|
||||
scope: Base
|
||||
settingsUpdated: Configurações atualizadas!
|
||||
user: Utilizador
|
||||
userCommands: Comandos
|
||||
userCommandsHelp:
|
||||
'Uma lista, separada com espaços, de comandos disponíveis para este
|
||||
utilizados. Exemplo:'
|
||||
userCreated: Utilizador criado!
|
||||
userDeleted: Utilizador eliminado!
|
||||
userManagement: Gestão de Utilizadores
|
||||
username: Nome de utilizador
|
||||
users: Utilizadores
|
||||
userUpdated: Utilizador atualizado!
|
||||
sidebar:
|
||||
help: Ajuda
|
||||
logout: Sair
|
||||
myFiles: Ficheiros
|
||||
newFile: Novo ficheiro
|
||||
newFolder: Nova pasta
|
||||
servedWith: Servido com
|
||||
settings: Configurações
|
||||
search:
|
||||
writeToSearch: Escreva aqui para pesquisar
|
||||
searchOrCommand: Pesquise ou execute um comando...
|
||||
searchOrSupportedCommand: 'Pesquise ou utilize um dos seus comandos:'
|
||||
search: Pesquise...
|
||||
type: Escreva e prima enter para pesquisar.
|
||||
pressToSearch: Prima enter para pesquisar.
|
||||
pressToExecute: Prima enter para executar.
|
||||
languages:
|
||||
en: Inglês
|
||||
pt: Português
|
||||
zhCN: Chinês (Simplificado)
|
||||
157
assets/src/i18n/zh-cn.yaml
Normal file
157
assets/src/i18n/zh-cn.yaml
Normal file
@@ -0,0 +1,157 @@
|
||||
buttons:
|
||||
cancel: 取消
|
||||
close: 关闭
|
||||
copy: 复制
|
||||
copyFile: 复制文件
|
||||
create: 创建
|
||||
delete: 删除
|
||||
download: 下载
|
||||
info: 信息
|
||||
more: 更多
|
||||
move: 移动
|
||||
moveFile: 移动文件
|
||||
new: 新
|
||||
next: 下一步
|
||||
ok: 确定
|
||||
previous: 以前
|
||||
rename: 重命名
|
||||
reportIssue: 报告问题
|
||||
save: 保存
|
||||
search: 搜索
|
||||
select: 选择
|
||||
selectMultiple: 选择多个
|
||||
switchView: 切换显示方式
|
||||
toggleSidebar: 切换侧边栏
|
||||
update: 更新
|
||||
upload: 上传
|
||||
errors:
|
||||
forbidden: 你被禁止访问.
|
||||
internal: 内部出现麻烦了.
|
||||
notFound: 找不到文件.
|
||||
files:
|
||||
folders: 文件夹
|
||||
files: 文件
|
||||
body: Body
|
||||
clear: 清理
|
||||
closePreview: 关闭预览
|
||||
home: 主页
|
||||
lastModified: 最后修改
|
||||
loading: 加载中...
|
||||
lonely: 这里没有任何文件...
|
||||
metadata: 元数据
|
||||
multipleSelectionEnabled: 启用多选模式(现在可以选择多个文件/文件夹)
|
||||
name: 名称
|
||||
size: 大小
|
||||
help:
|
||||
click: 选择文件或目录
|
||||
ctrl:
|
||||
click: 选择多个文件或目录
|
||||
f: 打开搜索框
|
||||
s: 保存文件或下载文件夹
|
||||
del: 删除 所选文件/文件夹
|
||||
doubleClick: 打开文件或目录
|
||||
esc: 清除 当前所有选择 或 关闭提示信息
|
||||
f1: 显示 当前帮助信息
|
||||
f2: 重命名 文件/文件夹
|
||||
help: 帮助
|
||||
login:
|
||||
password: 密码
|
||||
submit: 登录
|
||||
username: 用户名
|
||||
wrongCredentials: 账号或密码错误
|
||||
prompts:
|
||||
copy: 复制
|
||||
copyMessage: '请选择欲复制至的目录:'
|
||||
currentlyNavigating: '目前正在浏览:'
|
||||
deleteMessageMultiple: 你确定要删除这 {count} 个文件吗?
|
||||
deleteMessageSingle: 你确定要删除这个文件/文件夹吗?
|
||||
deleteTitle: 删除文件
|
||||
displayName: '名称:'
|
||||
download: 下载文件
|
||||
downloadMessage: 请选择要下载的压缩格式.
|
||||
error: 出了一点问题...
|
||||
fileInfo: 文件信息
|
||||
filesSelected: '选择 {count} 个文件.'
|
||||
lastModified: 最后修改
|
||||
move: 移动
|
||||
moveMessage: '请选择欲移动至的目录:'
|
||||
newDir: 新建目录
|
||||
newDirMessage: 请输入新建目录的名称.
|
||||
newFile: 新建文件
|
||||
newFileMessage: 请输入新建文件的名称.
|
||||
numberDirs: 目录数
|
||||
numberFiles: 文件数
|
||||
rename: 重命名
|
||||
renameMessage: '请输入新名称, 旧名称是:'
|
||||
show: 揭示
|
||||
size: 大小
|
||||
settings:
|
||||
admin: 管理员
|
||||
administrator: 管理员
|
||||
allowCommands: 执行命令(Linux 代码)
|
||||
allowEdit: 编辑、重命名或删除文件/目录.
|
||||
allowNew: 创建新文件和目录.
|
||||
avoidChanges: '(留空以避免更改)'
|
||||
changePassword: 更改密码
|
||||
commands: 命令(linux 代码)
|
||||
commandsHelp: >
|
||||
'Here you can set commands that are executed in the named events.
|
||||
每行一条命令. If the event is related to files, such as before and after saving,
|
||||
the environment variable "file" will be available with the path of the file.'
|
||||
commandsUpdated: 命令更新!
|
||||
customStylesheet: 自定义样式表
|
||||
examples: 例子
|
||||
globalSettings: 全局设置
|
||||
language: 语言
|
||||
newPassword: 您的新密码
|
||||
newPasswordConfirm: 重输一遍新密码
|
||||
newUser: 新建用户
|
||||
password: 密码
|
||||
passwordUpdated: 密码更新!
|
||||
permissions: 权限
|
||||
permissionsHelp: >
|
||||
'您可以将该用户设置为管理员 或单独选择各项权限. 如果选择 "管理员(Administrator)" ,
|
||||
将自动检查所有其他选项, 并且该用户可以管理其他用户.'
|
||||
pluginsUpdated: 插件设置更新!
|
||||
profileSettings: 配置文件设置
|
||||
ruleExample1: >
|
||||
'阻止用户访问每个文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore).'
|
||||
ruleExample2: 阻止用户访问其目录范围内任何名为 Caddyfile 的文件/文件夹.
|
||||
rules: 规则
|
||||
rulesHelp1: >
|
||||
'这里您可以为特定用户制定一组允许或不允许的规则,
|
||||
阻止的文件将不会显示到列表中, 用户将无法访问, 支持相对于用户的范围.'
|
||||
rulesHelp2: >
|
||||
每行一条规则, 必须以关键词 {0} 或 {1} 开头. 如果使用正则表达式,
|
||||
然后使用表达式或路径, 则需要在第二列单词加入 {2} .
|
||||
scope: 目录范围
|
||||
settingsUpdated: 设置更新!
|
||||
user: 用户
|
||||
userCommands: 用户命令(Linux 代码)
|
||||
userCommandsHelp: '一个以空格分割的列表, 用于指定该用户可以执行的命令(Linux 代码), 例如:'
|
||||
userCreated: 用户创建!
|
||||
userDeleted: 用户删除!
|
||||
userManagement: 用户管理
|
||||
username: 用户名
|
||||
users: 用户
|
||||
userUpdated: 用户更新!
|
||||
sidebar:
|
||||
help: 帮助
|
||||
logout: 注销
|
||||
myFiles: 我的文件
|
||||
newFile: 新建文件
|
||||
newFolder: 新建文件夹
|
||||
servedWith: 服务提供
|
||||
settings: 设置
|
||||
search:
|
||||
writeToSearch: 请输入要搜索的内容
|
||||
searchOrCommand: 搜索或者执行命令(Linux 代码)...
|
||||
searchOrSupportedCommand: '搜索或使用您支持使用的命令(一次只能执行一个命令):'
|
||||
search: 搜索...
|
||||
type: 键入并按 Enter 键(回车)进行搜索.
|
||||
pressToSearch: 按 Enter 键(回车)进行搜索.
|
||||
pressToExecute: 按 Enter 键(回车)执行.
|
||||
languages:
|
||||
en: English
|
||||
pt: Portuguese
|
||||
zhCN: Chinese (Simplified)
|
||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||
import App from './App'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
|
||||
Vue.config.productionTip = true
|
||||
|
||||
@@ -10,6 +11,7 @@ new Vue({
|
||||
el: '#app',
|
||||
store,
|
||||
router,
|
||||
i18n,
|
||||
template: '<App/>',
|
||||
components: { App }
|
||||
})
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Login from '@/components/Login'
|
||||
import Main from '@/components/Main'
|
||||
import Files from '@/components/Files'
|
||||
import Users from '@/components/Users'
|
||||
import User from '@/components/User'
|
||||
import GlobalSettings from '@/components/GlobalSettings'
|
||||
import ProfileSettings from '@/components/ProfileSettings'
|
||||
import error403 from '@/components/errors/403'
|
||||
import error404 from '@/components/errors/404'
|
||||
import error500 from '@/components/errors/500'
|
||||
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 Error403 from '@/views/errors/403'
|
||||
import Error404 from '@/views/errors/404'
|
||||
import Error500 from '@/views/errors/500'
|
||||
import auth from '@/utils/auth.js'
|
||||
import store from '@/store'
|
||||
|
||||
@@ -25,24 +25,18 @@ const router = new Router({
|
||||
component: Login,
|
||||
beforeEnter: function (to, from, next) {
|
||||
auth.loggedIn()
|
||||
.then(() => {
|
||||
next({ path: '/files' })
|
||||
})
|
||||
.catch(() => {
|
||||
document.title = 'Login'
|
||||
next()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: {
|
||||
path: '/files/'
|
||||
.then(() => {
|
||||
next({ path: '/files' })
|
||||
})
|
||||
.catch(() => {
|
||||
document.title = 'Login'
|
||||
next()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/*',
|
||||
component: Main,
|
||||
component: Layout,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
},
|
||||
@@ -75,17 +69,17 @@ const router = new Router({
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: error403
|
||||
component: Error403
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: 'Not Found',
|
||||
component: error404
|
||||
component: Error404
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: 'Internal Server Error',
|
||||
component: error500
|
||||
component: Error500
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
@@ -95,12 +89,6 @@ const router = new Router({
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/users/',
|
||||
redirect: {
|
||||
path: '/users'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/users/*',
|
||||
name: 'User',
|
||||
@@ -109,6 +97,12 @@ const router = new Router({
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/files',
|
||||
redirect: {
|
||||
path: '/files/'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/*',
|
||||
redirect: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import i18n from '@/i18n'
|
||||
|
||||
const mutations = {
|
||||
closeHovers: state => {
|
||||
state.show = null
|
||||
@@ -22,8 +24,10 @@ const mutations = {
|
||||
},
|
||||
setLoading: (state, value) => { state.loading = value },
|
||||
setReload: (state, value) => { state.reload = value },
|
||||
setUser: (state, value) => (state.user = value),
|
||||
setUserCSS: (state, value) => (state.user.css = value),
|
||||
setUser: (state, value) => {
|
||||
i18n.locale = value.locale
|
||||
state.user = value
|
||||
},
|
||||
setJWT: (state, value) => (state.jwt = value),
|
||||
multiple: (state, value) => (state.multiple = value),
|
||||
addSelected: (state, value) => (state.selected.push(value)),
|
||||
|
||||
@@ -2,7 +2,7 @@ import store from '@/store'
|
||||
|
||||
const ssl = (window.location.protocol === 'https:')
|
||||
|
||||
function removePrefix (url) {
|
||||
export function removePrefix (url) {
|
||||
if (url.startsWith('/files')) {
|
||||
return url.slice(6)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ function removePrefix (url) {
|
||||
return url
|
||||
}
|
||||
|
||||
function fetch (url) {
|
||||
export function fetch (url) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -24,10 +24,7 @@ function fetch (url) {
|
||||
resolve(JSON.parse(request.responseText))
|
||||
break
|
||||
default:
|
||||
reject({
|
||||
message: request.responseText,
|
||||
status: request.status
|
||||
})
|
||||
reject(new Error(request.status))
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -36,7 +33,7 @@ function fetch (url) {
|
||||
})
|
||||
}
|
||||
|
||||
function rm (url) {
|
||||
export function rm (url) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -57,7 +54,7 @@ function rm (url) {
|
||||
})
|
||||
}
|
||||
|
||||
function post (url, content = '') {
|
||||
export function post (url, content = '') {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -78,7 +75,7 @@ function post (url, content = '') {
|
||||
})
|
||||
}
|
||||
|
||||
function put (url, content = '') {
|
||||
export function put (url, content = '') {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -132,15 +129,15 @@ function moveCopy (items, copy = false) {
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
function move (items) {
|
||||
export function move (items) {
|
||||
return moveCopy(items)
|
||||
}
|
||||
|
||||
function copy (items) {
|
||||
export function copy (items) {
|
||||
return moveCopy(items, true)
|
||||
}
|
||||
|
||||
function checksum (url, algo) {
|
||||
export function checksum (url, algo) {
|
||||
url = removePrefix(url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -160,7 +157,7 @@ function checksum (url, algo) {
|
||||
})
|
||||
}
|
||||
|
||||
function command (url, command, onmessage, onclose) {
|
||||
export function command (url, command, onmessage, onclose) {
|
||||
let protocol = (ssl ? 'wss:' : 'ws:')
|
||||
url = removePrefix(url)
|
||||
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/command${url}`
|
||||
@@ -171,7 +168,7 @@ function command (url, command, onmessage, onclose) {
|
||||
conn.onclose = onclose
|
||||
}
|
||||
|
||||
function search (url, search, onmessage, onclose) {
|
||||
export function search (url, search, onmessage, onclose) {
|
||||
let protocol = (ssl ? 'wss:' : 'ws:')
|
||||
url = removePrefix(url)
|
||||
url = `${protocol}//${window.location.host}${store.state.baseURL}/api/search${url}`
|
||||
@@ -182,7 +179,7 @@ function search (url, search, onmessage, onclose) {
|
||||
conn.onclose = onclose
|
||||
}
|
||||
|
||||
function download (format, ...files) {
|
||||
export function download (format, ...files) {
|
||||
let url = `${store.state.baseURL}/api/download`
|
||||
|
||||
if (files.length === 1) {
|
||||
@@ -206,7 +203,59 @@ function download (format, ...files) {
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
function getUsers () {
|
||||
export function getSettings () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/settings/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve(JSON.parse(request.responseText))
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
export function updateSettings (param, which) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = {
|
||||
what: 'settings',
|
||||
which: which,
|
||||
data: {}
|
||||
}
|
||||
|
||||
data.data[which] = param
|
||||
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => { reject(error) }
|
||||
request.send(JSON.stringify(data))
|
||||
})
|
||||
}
|
||||
|
||||
// USERS
|
||||
|
||||
export function getUsers () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/users/`, true)
|
||||
@@ -227,7 +276,7 @@ function getUsers () {
|
||||
})
|
||||
}
|
||||
|
||||
function getUser (id) {
|
||||
export function getUser (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
|
||||
@@ -248,7 +297,7 @@ function getUser (id) {
|
||||
})
|
||||
}
|
||||
|
||||
function newUser (user) {
|
||||
export function newUser (user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${store.state.baseURL}/api/users/`, true)
|
||||
@@ -265,11 +314,15 @@ function newUser (user) {
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify(user))
|
||||
request.send(JSON.stringify({
|
||||
what: 'user',
|
||||
which: 'new',
|
||||
data: user
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
export function updateUser (user, which) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
|
||||
@@ -286,11 +339,15 @@ function updateUser (user) {
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify(user))
|
||||
request.send(JSON.stringify({
|
||||
what: 'user',
|
||||
which: (typeof which === 'string') ? which : 'all',
|
||||
data: user
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function deleteUser (id) {
|
||||
export function deleteUser (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
|
||||
@@ -311,133 +368,8 @@ function deleteUser (id) {
|
||||
})
|
||||
}
|
||||
|
||||
function updatePassword (password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/users/change-password`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify({ 'password': password }))
|
||||
})
|
||||
}
|
||||
|
||||
function updateCSS (css) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/users/change-css`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify({ 'css': css }))
|
||||
})
|
||||
}
|
||||
|
||||
function getCommands () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/commands/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve(JSON.parse(request.responseText))
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
function updateCommands (commands) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/commands/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify(commands))
|
||||
})
|
||||
}
|
||||
|
||||
function getPlugins () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('GET', `${store.state.baseURL}/api/plugins/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve(JSON.parse(request.responseText))
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
|
||||
function updatePlugins (data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('PUT', `${store.state.baseURL}/api/plugins/`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||
|
||||
request.onload = () => {
|
||||
switch (request.status) {
|
||||
case 200:
|
||||
resolve()
|
||||
break
|
||||
default:
|
||||
reject(request.responseText)
|
||||
break
|
||||
}
|
||||
}
|
||||
request.onerror = (error) => reject(error)
|
||||
request.send(JSON.stringify(data))
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
removePrefix,
|
||||
delete: rm,
|
||||
fetch,
|
||||
checksum,
|
||||
@@ -448,16 +380,13 @@ export default {
|
||||
command,
|
||||
search,
|
||||
download,
|
||||
getUser,
|
||||
// other things
|
||||
getSettings,
|
||||
updateSettings,
|
||||
// User things
|
||||
newUser,
|
||||
updateUser,
|
||||
getUser,
|
||||
getUsers,
|
||||
updatePassword,
|
||||
updateCSS,
|
||||
getCommands,
|
||||
updateCommands,
|
||||
removePrefix,
|
||||
getPlugins,
|
||||
updatePlugins,
|
||||
updateUser,
|
||||
deleteUser
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ function loggedIn () {
|
||||
parseToken(request.responseText)
|
||||
resolve()
|
||||
} else {
|
||||
reject()
|
||||
reject(new Error(request.responseText))
|
||||
}
|
||||
}
|
||||
request.onerror = () => reject()
|
||||
request.onerror = () => reject(new Error('Could not finish the request'))
|
||||
request.send()
|
||||
})
|
||||
}
|
||||
@@ -45,7 +45,7 @@ function login (user, password) {
|
||||
reject(request.responseText)
|
||||
}
|
||||
}
|
||||
request.onerror = () => reject()
|
||||
request.onerror = () => reject(new Error('Could not finish the request'))
|
||||
request.send(JSON.stringify(data))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="breadcrumbs">
|
||||
<router-link to="/files/">
|
||||
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||
<i class="material-icons">home</i>
|
||||
</router-link>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="error">
|
||||
<not-found v-if="error === 404"></not-found>
|
||||
<forbidden v-else-if="error === 403"></forbidden>
|
||||
<not-found v-if="error.message === '404'"></not-found>
|
||||
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||
<internal-error v-else></internal-error>
|
||||
</div>
|
||||
<editor v-else-if="isEditor"></editor>
|
||||
@@ -20,7 +20,7 @@
|
||||
<preview v-else-if="isPreview"></preview>
|
||||
<div v-else>
|
||||
<h2 class="message">
|
||||
<span>Loading...</span>
|
||||
<span>{{ $t('files.loading') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,9 +30,9 @@
|
||||
import Forbidden from './errors/403'
|
||||
import NotFound from './errors/404'
|
||||
import InternalError from './errors/500'
|
||||
import Preview from './Preview'
|
||||
import Listing from './Listing'
|
||||
import Editor from './Editor'
|
||||
import Preview from '@/components/files/Preview'
|
||||
import Listing from '@/components/files/Listing'
|
||||
import Editor from '@/components/files/Editor'
|
||||
import api from '@/utils/api'
|
||||
import { mapGetters, mapState, mapMutations } from 'vuex'
|
||||
|
||||
@@ -116,20 +116,14 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
window.addEventListener('scroll', event => {
|
||||
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
|
||||
|
||||
let top = 112 - window.scrollY
|
||||
|
||||
if (top < 64) {
|
||||
top = 64
|
||||
}
|
||||
|
||||
document.querySelector('#listing.list .item.header').style.top = top + 'px'
|
||||
})
|
||||
window.addEventListener('scroll', this.scroll)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
window.removeEventListener('scroll', this.scroll)
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.commit('updateRequest', {})
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'setLoading' ]),
|
||||
@@ -149,25 +143,19 @@ export default {
|
||||
if (url[0] !== '/') url = '/' + url
|
||||
|
||||
api.fetch(url)
|
||||
.then((req) => {
|
||||
if (!url.endsWith('/') && req.url.endsWith('/')) {
|
||||
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
|
||||
}
|
||||
.then((req) => {
|
||||
if (!url.endsWith('/') && req.url.endsWith('/')) {
|
||||
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
|
||||
}
|
||||
|
||||
this.$store.commit('updateRequest', req)
|
||||
document.title = req.name
|
||||
this.setLoading(false)
|
||||
})
|
||||
.catch(error => {
|
||||
this.setLoading(false)
|
||||
|
||||
if (typeof error === 'object') {
|
||||
this.error = error.status
|
||||
return
|
||||
}
|
||||
|
||||
this.error = error
|
||||
})
|
||||
this.$store.commit('updateRequest', req)
|
||||
document.title = req.name
|
||||
this.setLoading(false)
|
||||
})
|
||||
.catch(error => {
|
||||
this.setLoading(false)
|
||||
this.error = error
|
||||
})
|
||||
},
|
||||
keyEvent (event) {
|
||||
// Esc!
|
||||
@@ -217,11 +205,21 @@ export default {
|
||||
|
||||
if (this.req.kind !== 'editor') {
|
||||
document.getElementById('download-button').click()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scroll (event) {
|
||||
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
|
||||
|
||||
let top = 112 - window.scrollY
|
||||
|
||||
if (top < 64) {
|
||||
top = 64
|
||||
}
|
||||
|
||||
document.querySelector('#listing.list .item.header').style.top = top + 'px'
|
||||
},
|
||||
openSidebar () {
|
||||
this.$store.commit('showHover', 'sidebar')
|
||||
},
|
||||
@@ -1,17 +1,25 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>Global Settings</h1>
|
||||
|
||||
<ul>
|
||||
<li><router-link to="/settings/profile">Go to Profile Settings</router-link></li>
|
||||
<li><router-link to="/users">Go to User Management</router-link></li>
|
||||
<ul 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="savePlugin" v-if="plugins.length > 0">
|
||||
<template v-for="plugin in plugins">
|
||||
<h2>{{ capitalize(plugin.name) }}</h2>
|
||||
|
||||
<p v-for="field in plugin.fields" :key="field.name">
|
||||
<p v-for="field in plugin.fields" :key="field.variable">
|
||||
<label v-if="field.type !== 'checkbox'">{{ field.name }}</label>
|
||||
<input v-if="field.type === 'text'" type="text" v-model.trim="field.value">
|
||||
<input v-else-if="field.type === 'checkbox'" type="checkbox" v-model.trim="field.value">
|
||||
@@ -23,11 +31,9 @@
|
||||
</form>
|
||||
|
||||
<form @submit="saveCommands">
|
||||
<h2>Commands</h2>
|
||||
<h2>{{ $t('settings.commands') }}</h2>
|
||||
|
||||
<p class="small">Here you can set commands that are executed in the named events. You write one command
|
||||
per line. If the event is related to files, such as before and after saving, the environment variable
|
||||
<code>file</code> will be available with the path of the file.</p>
|
||||
<p class="small">{{ $t('settings.commandsHelp') }}</p>
|
||||
|
||||
<template v-for="command in commands">
|
||||
<h3>{{ capitalize(command.name) }}</h3>
|
||||
@@ -42,7 +48,7 @@
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
import { getSettings, updateSettings } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
@@ -56,63 +62,20 @@ export default {
|
||||
...mapState([ 'user' ])
|
||||
},
|
||||
created () {
|
||||
api.getCommands()
|
||||
.then(commands => {
|
||||
for (let key in commands) {
|
||||
getSettings()
|
||||
.then(settings => {
|
||||
for (let key in settings.plugins) {
|
||||
this.plugins.push(this.parsePlugin(key, settings.plugins[key]))
|
||||
}
|
||||
|
||||
for (let key in settings.commands) {
|
||||
this.commands.push({
|
||||
name: key,
|
||||
value: commands[key].join('\n')
|
||||
value: settings.commands[key].join('\n')
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
|
||||
api.getPlugins()
|
||||
.then(plugins => {
|
||||
console.log(plugins)
|
||||
let plugin = {}
|
||||
|
||||
for (let key in plugins) {
|
||||
plugin.name = key
|
||||
plugin.fields = []
|
||||
|
||||
for (let field in plugins[key]) {
|
||||
let value = plugins[key][field]
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
plugin.fields.push({
|
||||
name: field,
|
||||
type: 'text',
|
||||
original: 'array',
|
||||
value: value.join(' ')
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
plugin.fields.push({
|
||||
name: field,
|
||||
type: 'checkbox',
|
||||
original: 'boolean',
|
||||
value: value
|
||||
})
|
||||
break
|
||||
default:
|
||||
plugin.fields.push({
|
||||
name: field,
|
||||
type: 'text',
|
||||
original: 'text',
|
||||
value: value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.plugins.push(plugin)
|
||||
}
|
||||
})
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'showSuccess', 'showError' ]),
|
||||
@@ -141,8 +104,8 @@ export default {
|
||||
commands[command.name] = value
|
||||
}
|
||||
|
||||
api.updateCommands(commands)
|
||||
.then(() => { this.showSuccess('Commands updated!') })
|
||||
updateSettings(commands, 'commands')
|
||||
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) })
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
savePlugin (event) {
|
||||
@@ -153,7 +116,7 @@ export default {
|
||||
let p = {}
|
||||
|
||||
for (let field of plugin.fields) {
|
||||
p[field.name] = field.value
|
||||
p[field.variable] = field.value
|
||||
|
||||
if (field.original === 'array') {
|
||||
let val = field.value.split(' ')
|
||||
@@ -161,18 +124,53 @@ export default {
|
||||
val.shift()
|
||||
}
|
||||
|
||||
p[field.name] = val
|
||||
p[field.variable] = val
|
||||
}
|
||||
}
|
||||
|
||||
plugins[plugin.name] = p
|
||||
}
|
||||
|
||||
console.log(plugins)
|
||||
|
||||
api.updatePlugins(plugins)
|
||||
.then(() => { this.showSuccess('Plugins settings updated!') })
|
||||
updateSettings(plugins, 'plugins')
|
||||
.then(() => { this.showSuccess(this.$t('settings.pluginsUpdated')) })
|
||||
.catch(error => { this.showError(error) })
|
||||
},
|
||||
parsePlugin (name, plugin) {
|
||||
let obj = {
|
||||
name: name,
|
||||
fields: []
|
||||
}
|
||||
|
||||
for (let option of plugin) {
|
||||
let value = option.value
|
||||
|
||||
let field = {
|
||||
name: option.name,
|
||||
variable: option.variable,
|
||||
type: 'text',
|
||||
original: 'text',
|
||||
value: value
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
field.original = 'array'
|
||||
field.value = value.join(' ')
|
||||
|
||||
obj.fields.push(field)
|
||||
continue
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
field.type = 'checkbox'
|
||||
field.original = 'boolean'
|
||||
break
|
||||
}
|
||||
|
||||
obj.fields.push(field)
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from './Search'
|
||||
import Sidebar from './Sidebar'
|
||||
import Prompts from './prompts/Prompts'
|
||||
import SiteHeader from './Header'
|
||||
import Search from '@/components/Search'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import Prompts from '@/components/prompts/Prompts'
|
||||
import SiteHeader from '@/components/Header'
|
||||
|
||||
export default {
|
||||
name: 'main',
|
||||
name: 'layout',
|
||||
components: {
|
||||
Search,
|
||||
Sidebar,
|
||||
42
assets/src/views/Login.vue
Normal file
42
assets/src/views/Login.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div id="login">
|
||||
<form @submit="submit">
|
||||
<img src="../assets/logo.svg" alt="File Manager">
|
||||
<h1>File Manager</h1>
|
||||
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
|
||||
<input type="text" v-model="username" :placeholder="$t('login.username')">
|
||||
<input type="password" v-model="password" :placeholder="$t('login.password')">
|
||||
<input type="submit" :value="$t('login.submit')">
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import auth from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
data: function () {
|
||||
return {
|
||||
wrong: false,
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
let redirect = this.$route.query.redirect
|
||||
if (redirect === '' || redirect === undefined || redirect === null) {
|
||||
redirect = '/files/'
|
||||
}
|
||||
|
||||
auth.login(this.username, this.password)
|
||||
.then(() => { this.$router.push({ path: redirect }) })
|
||||
.catch(() => { this.wrong = true })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
103
assets/src/views/ProfileSettings.vue
Normal file
103
assets/src/views/ProfileSettings.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<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>
|
||||
@@ -1,58 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="save" class="dashboard">
|
||||
<h1 v-if="id === 0">New User</h1>
|
||||
<h1 v-else>User {{ username }}</h1>
|
||||
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
|
||||
<h1 v-else>{{ $t('settings.user') }} {{ username }}</h1>
|
||||
|
||||
<p><label for="username">Username</label><input type="text" v-model="username" id="username"></p>
|
||||
<p><label for="password">Password</label><input type="password" :placeholder="passwordPlaceholder" v-model="password" id="password"></p>
|
||||
<p><label for="scope">Scope</label><input type="text" v-model="filesystem" id="scope"></p>
|
||||
<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>Permissions</h2>
|
||||
<h2>{{ $t('settings.permissions') }}</h2>
|
||||
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
|
||||
|
||||
<p class="small">You can set the user to be an administrator or choose the permissions individually.
|
||||
If you select "Administrator", all of the other options will be automatically checked.
|
||||
The management of users remains a privilege of an administrator.</p>
|
||||
|
||||
<p><input type="checkbox" v-model="admin"> Administrator</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowNew"> Create new files and directories</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowEdit"> Edit, rename and delete files or directories.</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="allowCommands"> Execute commands</p>
|
||||
<p><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-for="(value, key) in permissions" :key="key">
|
||||
<input type="checkbox" :disabled="admin" v-model="permissions[key]"> {{ capitalize(key) }}
|
||||
</p>
|
||||
|
||||
<h3>Commands</h3>
|
||||
|
||||
<p class="small">A space separated list with the available commands for this user. Example: <i>git svn hg</i>.</p>
|
||||
|
||||
<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>Rules</h2>
|
||||
<h2>{{ $t('settings.rules') }}</h2>
|
||||
|
||||
<p class="small">Here you can define a set of allow and disallow rules for this specific user. The blocked files won't
|
||||
show up in the listings and they won't be accessible to the user. We support regex and paths relative to
|
||||
the user's scope.</p>
|
||||
<p class="small">{{ $t('settings.rulesHelp1') }}</p>
|
||||
|
||||
<p class="small">Each rule goes in one different line and must start with the keyword <code>allow</code> or <code>disallow</code>.
|
||||
Then you should write <code>regex</code> if you are using a regular expression and then the expression or the path.</p>
|
||||
<i18n path="settings.rulesHelp2" tag="p" class="small">
|
||||
<code>allow</code><code>disallow</code><code>regex</code>
|
||||
</i18n>
|
||||
|
||||
<p class="small"><strong>Examples</strong></p>
|
||||
<p class="small"><strong>{{ $t('settings.examples') }}</strong></p>
|
||||
|
||||
<ul class="small">
|
||||
<li><code>disallow regex \\/\\..+</code> - prevents the access to any dot file (such as .git, .gitignore) in every folder.</li>
|
||||
<li><code>disallow /Caddyfile</code> - blocks the access to the file named <i>Caddyfile</i> on the root of the scope</li>
|
||||
<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>Custom Stylesheet</h2>
|
||||
<h2>{{ $t('settings.customStylesheet') }}</h2>
|
||||
|
||||
<textarea name="css"></textarea>
|
||||
|
||||
<p>
|
||||
<button v-if="id !== 0" @click.prevent="deletePrompt" type="button" class="delete">Delete</button>
|
||||
<input type="submit" value="Save">
|
||||
<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>
|
||||
|
||||
@@ -60,8 +58,13 @@
|
||||
<h3>Delete User</h3>
|
||||
<p>Are you sure you want to delete this user?</p>
|
||||
<div>
|
||||
<button @click="deleteUser" autofocus>Delete</button>
|
||||
<button @click="closeHovers" class="cancel">Cancel</button>
|
||||
<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>
|
||||
@@ -69,10 +72,12 @@
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import api from '@/utils/api'
|
||||
import { getUser, newUser, updateUser, deleteUser } from '@/utils/api'
|
||||
import Languages from '@/components/Languages'
|
||||
|
||||
export default {
|
||||
name: 'user',
|
||||
components: { Languages },
|
||||
data: () => {
|
||||
return {
|
||||
id: 0,
|
||||
@@ -85,6 +90,7 @@ export default {
|
||||
username: '',
|
||||
filesystem: '',
|
||||
rules: '',
|
||||
locale: '',
|
||||
css: '',
|
||||
commands: ''
|
||||
}
|
||||
@@ -92,7 +98,7 @@ export default {
|
||||
computed: {
|
||||
passwordPlaceholder () {
|
||||
if (this.$route.path === '/users/new') return ''
|
||||
return '(leave blank to avoid changes)'
|
||||
return this.$t('settings.avoidChanges')
|
||||
}
|
||||
},
|
||||
created () {
|
||||
@@ -119,7 +125,7 @@ export default {
|
||||
user = 'base'
|
||||
}
|
||||
|
||||
api.getUser(user).then(user => {
|
||||
getUser(user).then(user => {
|
||||
this.id = user.ID
|
||||
this.admin = user.admin
|
||||
this.allowCommands = user.allowCommands
|
||||
@@ -130,6 +136,7 @@ export default {
|
||||
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) {
|
||||
@@ -173,6 +180,7 @@ export default {
|
||||
this.username = ''
|
||||
this.filesystem = ''
|
||||
this.rules = ''
|
||||
this.locale = ''
|
||||
this.css = ''
|
||||
this.commands = ''
|
||||
},
|
||||
@@ -182,9 +190,9 @@ export default {
|
||||
deleteUser (event) {
|
||||
event.preventDefault()
|
||||
|
||||
api.deleteUser(this.id).then(location => {
|
||||
deleteUser(this.id).then(location => {
|
||||
this.$router.push({ path: '/users' })
|
||||
this.$store.commit('showSuccess', 'User deleted!')
|
||||
this.$store.commit('showSuccess', this.$t('settings.userDeleted'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
@@ -194,9 +202,9 @@ export default {
|
||||
let user = this.parseForm()
|
||||
|
||||
if (this.$route.path === '/users/new') {
|
||||
api.newUser(user).then(location => {
|
||||
newUser(user).then(location => {
|
||||
this.$router.push({ path: location })
|
||||
this.$store.commit('showSuccess', 'User created!')
|
||||
this.$store.commit('showSuccess', this.$t('settings.userCreated'))
|
||||
}).catch(e => {
|
||||
this.$store.commit('showError', e)
|
||||
})
|
||||
@@ -204,8 +212,12 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
api.updateUser(user).then(location => {
|
||||
this.$store.commit('showSuccess', 'User updated!')
|
||||
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)
|
||||
})
|
||||
@@ -222,6 +234,7 @@ export default {
|
||||
allowEdit: this.allowEdit,
|
||||
permissions: this.permissions,
|
||||
css: this.css,
|
||||
locale: this.locale,
|
||||
commands: this.commands.split(' '),
|
||||
rules: []
|
||||
}
|
||||
@@ -269,7 +282,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>Users <router-link to="/users/new"><button>New</button></router-link></h1>
|
||||
<h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Admin</th>
|
||||
<th>Scope</th>
|
||||
<th>{{ $t('settings.username') }}</th>
|
||||
<th>{{ $t('settings.admin') }}</th>
|
||||
<th>{{ $t('settings.scope') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">error</i>
|
||||
<span>You're not welcome here.</span>
|
||||
<span>{{ $t('errors.forbidden') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">gps_off</i>
|
||||
<span>This location can't be reached.</span>
|
||||
<span>{{ $t('errors.notFound') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<h2 class="message">
|
||||
<i class="material-icons">error_outline</i>
|
||||
<span>Something really went wrong.</span>
|
||||
<span>{{ $t('errors.internal') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
23
auth.go
23
auth.go
@@ -15,6 +15,11 @@ import (
|
||||
|
||||
// authHandler proccesses the authentication for the user.
|
||||
func authHandler(c *RequestContext, 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
|
||||
if r.Body == nil {
|
||||
@@ -27,7 +32,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
|
||||
}
|
||||
|
||||
// Checks if the user exists.
|
||||
u, ok := c.FM.Users[cred.Username]
|
||||
u, ok := c.Users[cred.Username]
|
||||
if !ok {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -56,6 +61,7 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
// claims is the JWT claims.
|
||||
type claims struct {
|
||||
User
|
||||
NoAuth bool `json:"noAuth"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
@@ -70,6 +76,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
|
||||
// Builds the claims.
|
||||
claims := claims{
|
||||
u,
|
||||
c.NoAuth,
|
||||
jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
|
||||
Issuer: "File Manager",
|
||||
@@ -78,14 +85,15 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
|
||||
|
||||
// Creates the token and signs it.
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
string, err := token.SignedString(c.FM.key)
|
||||
signed, err := token.SignedString(c.key)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Writes the token.
|
||||
w.Write([]byte(string))
|
||||
w.Header().Set("Content-Type", "cty")
|
||||
w.Write([]byte(signed))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -112,8 +120,13 @@ 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) {
|
||||
if c.NoAuth {
|
||||
c.User = c.DefaultUser
|
||||
return true, c.User
|
||||
}
|
||||
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return c.FM.key, nil
|
||||
return c.key, nil
|
||||
}
|
||||
var claims claims
|
||||
token, err := request.ParseFromRequestWithClaims(r,
|
||||
@@ -126,7 +139,7 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
u, ok := c.FM.Users[claims.User.Username]
|
||||
u, ok := c.Users[claims.User.Username]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
11
build.sh
11
build.sh
@@ -1,6 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Install rice tool if not present
|
||||
if ! [ -x "$(command -v rice)" ]; then
|
||||
go get github.com/GeertJohan/go.rice/rice
|
||||
fi
|
||||
|
||||
# Clean the dist folder and build the assets
|
||||
rm -rf assets/dist
|
||||
npm run build
|
||||
rice embed-go
|
||||
cd ./caddy/hugo
|
||||
|
||||
# Embed the assets using rice
|
||||
rice embed-go
|
||||
|
||||
@@ -1,192 +1,168 @@
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"log"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
rice "github.com/GeertJohan/go.rice"
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/varutils"
|
||||
"github.com/robfig/cron"
|
||||
"github.com/hacdias/filemanager/plugins"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
type hugo struct {
|
||||
// Website root
|
||||
Root string `description:"The relative or absolute path to the place where your website is located."`
|
||||
// Public folder
|
||||
Public string `description:"The relative or absolute path to the public folder."`
|
||||
// Hugo executable path
|
||||
Exe string `description:"The absolute path to the Hugo executable or the command to execute."`
|
||||
// Hugo arguments
|
||||
Args []string `description:"The arguments to run when running Hugo"`
|
||||
// Indicates if we should clean public before a new publish.
|
||||
CleanPublic bool `description:"Indicates if the public folder should be cleaned before publishing the website."`
|
||||
// setup configures a new FileManager middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := parse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: admin interface to cgange options
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return plugin{Configs: configs, Next: next}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h hugo) BeforeAPI(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// If we are using the 'magic url' for the settings, we should redirect the
|
||||
// request for the acutual path.
|
||||
if r.URL.Path == "/settings/" || r.URL.Path == "/settings" {
|
||||
var frontmatter string
|
||||
var err error
|
||||
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
||||
var (
|
||||
configs []*filemanager.FileManager
|
||||
)
|
||||
|
||||
if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil {
|
||||
frontmatter = "yaml"
|
||||
for c.Next() {
|
||||
// hugo [directory] [admin] {
|
||||
// database path
|
||||
// }
|
||||
directory := "."
|
||||
admin := "/admin"
|
||||
database := ""
|
||||
|
||||
// Get the baseURL and baseScope
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) >= 1 {
|
||||
directory = args[0]
|
||||
}
|
||||
|
||||
if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil {
|
||||
frontmatter = "json"
|
||||
if len(args) > 1 {
|
||||
admin = args[1]
|
||||
}
|
||||
|
||||
if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil {
|
||||
frontmatter = "toml"
|
||||
}
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "database":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
r.URL.Path = "/config." + frontmatter
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// From here on, we only care about 'hugo' router so we can bypass
|
||||
// the others.
|
||||
if c.Router != "hugo" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// If we are not using HTTP Post, we shall return Method Not Allowed
|
||||
// since we are only working with this method.
|
||||
if r.Method != http.MethodPost {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
// If we are creating a file built from an archetype.
|
||||
if r.Header.Get("Archetype") != "" {
|
||||
if !c.User.AllowNew {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
archetype := r.Header.Get("archetype")
|
||||
|
||||
ext := filepath.Ext(filename)
|
||||
|
||||
// If the request isn't for a markdown file, we can't
|
||||
// handle it.
|
||||
if ext != ".markdown" && ext != ".md" {
|
||||
return http.StatusBadRequest, errUnsupportedFileType
|
||||
}
|
||||
|
||||
// Tries to create a new file based on this archetype.
|
||||
args := []string{"new", filename, "--kind", archetype}
|
||||
if err := Run(h.Exe, args, h.Root); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Writes the location of the new file to the Header.
|
||||
w.Header().Set("Location", "/files/content/"+filename)
|
||||
return http.StatusCreated, nil
|
||||
}
|
||||
|
||||
// If we are trying to regenerate the website.
|
||||
if r.Header.Get("Regenerate") == "true" {
|
||||
if !c.User.Permissions["allowPublish"] {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
|
||||
// Before save command handler.
|
||||
if err := c.FM.Runner("before_publish", filename); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// We only run undraft command if it is a file.
|
||||
if !strings.HasSuffix(filename, "/") {
|
||||
args := []string{"undraft", filename}
|
||||
if err := Run(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") {
|
||||
return http.StatusInternalServerError, err
|
||||
database = c.Val()
|
||||
}
|
||||
}
|
||||
|
||||
// Regenerates the file
|
||||
h.run(false)
|
||||
caddyConf := httpserver.GetConfig(c)
|
||||
|
||||
// Executed the before publish command.
|
||||
if err := c.FM.Runner("before_publish", filename); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
path := filepath.Join(caddy.AssetsPath(), "hugo")
|
||||
err := os.MkdirAll(path, 0700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
if r.Header.Get("Schedule") != "" {
|
||||
if !c.User.Permissions["allowPublish"] {
|
||||
return http.StatusForbidden, nil
|
||||
// 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)
|
||||
}
|
||||
|
||||
return h.schedule(c, w, r)
|
||||
// If there is no database path on the settings,
|
||||
// store one in .caddy/hugo/{name}.db.
|
||||
if database == "" {
|
||||
// The name of the database is the hashed value of a string composed
|
||||
// by the host, address path and the baseurl of this File Manager
|
||||
// instance.
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + admin))
|
||||
sha := hex.EncodeToString(hasher.Sum(nil))
|
||||
database = filepath.Join(path, sha+".db")
|
||||
|
||||
fmt.Println("[WARNING] A database is going to be created for your Hugo instace at " + database +
|
||||
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
||||
}
|
||||
|
||||
m, err := filemanager.New(database, filemanager.User{
|
||||
AllowCommands: true,
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
Permissions: map[string]bool{},
|
||||
Commands: []string{"git", "svn", "hg"},
|
||||
Rules: []*filemanager.Rule{{
|
||||
Regex: true,
|
||||
Allow: false,
|
||||
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
||||
}},
|
||||
CSS: "",
|
||||
FileSystem: fileutils.Dir(directory),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize the default settings for Hugo.
|
||||
hugo := &plugins.Hugo{
|
||||
Root: directory,
|
||||
Public: filepath.Join(directory, "public"),
|
||||
Args: []string{},
|
||||
CleanPublic: true,
|
||||
}
|
||||
|
||||
// Try to find the Hugo executable path.
|
||||
if err = hugo.Find(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Attaches Hugo plugin to this file manager instance.
|
||||
err = m.ActivatePlugin("hugo", hugo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.SetBaseURL(admin)
|
||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
||||
configs = append(configs, m)
|
||||
}
|
||||
|
||||
return http.StatusNotFound, nil
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (h hugo) AfterAPI(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (h hugo) JavaScript() string {
|
||||
return rice.MustFindBox("./assets/").MustString("hugo.js")
|
||||
}
|
||||
|
||||
// run runs Hugo with the define arguments.
|
||||
func (h hugo) run(force bool) {
|
||||
// If the CleanPublic option is enabled, clean it.
|
||||
if h.CleanPublic {
|
||||
os.RemoveAll(h.Public)
|
||||
}
|
||||
|
||||
// Prevent running if watching is enabled
|
||||
if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force {
|
||||
if len(h.Args) > pos && h.Args[pos+1] != "false" {
|
||||
return
|
||||
// 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
|
||||
}
|
||||
|
||||
if len(h.Args) == pos+1 {
|
||||
return
|
||||
}
|
||||
p.Configs[i].ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if err := Run(h.Exe, h.Args, h.Root); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return p.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// schedule schedules a post to be published later.
|
||||
func (h hugo) schedule(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t, err := time.Parse("2006-01-02T15:04", r.Header.Get("Schedule"))
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
path = filepath.Clean(path)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
scheduler := cron.New()
|
||||
scheduler.AddFunc(t.Format("05 04 15 02 01 *"), func() {
|
||||
args := []string{"undraft", path}
|
||||
if err := Run(h.Exe, args, h.Root); err != nil {
|
||||
log.Printf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.run(false)
|
||||
func init() {
|
||||
caddy.RegisterPlugin("hugo", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
|
||||
scheduler.Start()
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
Next httpserver.Handler
|
||||
Configs []*filemanager.FileManager
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,188 +0,0 @@
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
var (
|
||||
errHugoNotFound = errors.New("It seems that tou don't have 'hugo' on your PATH")
|
||||
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
|
||||
)
|
||||
|
||||
// setup configures a new FileManager middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
configs, err := parse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return plugin{Configs: configs, Next: next}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) {
|
||||
var (
|
||||
configs []*filemanager.FileManager
|
||||
)
|
||||
|
||||
for c.Next() {
|
||||
// hugo [directory] [admin] {
|
||||
// database path
|
||||
// }
|
||||
directory := "."
|
||||
admin := "/admin"
|
||||
database := ""
|
||||
|
||||
// Get the baseURL and baseScope
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) >= 1 {
|
||||
directory = args[0]
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
admin = args[1]
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "database":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
database = c.Val()
|
||||
}
|
||||
}
|
||||
|
||||
caddyConf := httpserver.GetConfig(c)
|
||||
|
||||
path := filepath.Join(caddy.AssetsPath(), "hugo")
|
||||
err := os.MkdirAll(path, 0700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if there is a database path and it is not absolute,
|
||||
// it will be relative to ".caddy" folder.
|
||||
if !filepath.IsAbs(database) && database != "" {
|
||||
database = filepath.Join(path, database)
|
||||
}
|
||||
|
||||
// If there is no database path on the settings,
|
||||
// store one in .caddy/hugo/{name}.db.
|
||||
if database == "" {
|
||||
// The name of the database is the hashed value of a string composed
|
||||
// by the host, address path and the baseurl of this File Manager
|
||||
// instance.
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + admin))
|
||||
sha := hex.EncodeToString(hasher.Sum(nil))
|
||||
database = filepath.Join(path, sha+".db")
|
||||
|
||||
fmt.Println("[WARNING] A database is going to be created for your Hugo instace at " + database +
|
||||
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
|
||||
}
|
||||
|
||||
m, err := filemanager.New(database, filemanager.User{
|
||||
AllowCommands: true,
|
||||
AllowEdit: true,
|
||||
AllowNew: true,
|
||||
Permissions: map[string]bool{},
|
||||
Commands: []string{"git", "svn", "hg"},
|
||||
Rules: []*filemanager.Rule{{
|
||||
Regex: true,
|
||||
Allow: false,
|
||||
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
|
||||
}},
|
||||
CSS: "",
|
||||
FileSystem: fileutils.Dir(directory),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize the default settings for Hugo.
|
||||
hugo := &hugo{
|
||||
Root: directory,
|
||||
Public: filepath.Join(directory, "public"),
|
||||
Args: []string{},
|
||||
CleanPublic: true,
|
||||
}
|
||||
|
||||
// Try to find the Hugo executable path.
|
||||
if hugo.Exe, err = exec.LookPath("hugo"); err != nil {
|
||||
return nil, errHugoNotFound
|
||||
}
|
||||
|
||||
err = m.RegisterPlugin("hugo", hugo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.RegisterEventType("before_publish")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.RegisterEventType("after_publish")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.RegisterPermission("allowPublish", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.SetBaseURL(admin)
|
||||
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
|
||||
configs = append(configs, m)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
func (p plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for i := range p.Configs {
|
||||
// Checks if this Path should be handled by File Manager.
|
||||
if !httpserver.Path(r.URL.Path).Matches(p.Configs[i].BaseURL) {
|
||||
continue
|
||||
}
|
||||
|
||||
p.Configs[i].ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return p.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("hugo", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
Next httpserver.Handler
|
||||
Configs []*filemanager.FileManager
|
||||
}
|
||||
@@ -1,109 +1,190 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/hacdias/filemanager/plugins"
|
||||
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/fileutils"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// confFile contains the configuration file for this File Manager instance.
|
||||
// If the user chooses to use a configuration file, the flags will be ignored.
|
||||
type confFile struct {
|
||||
Database string `json:"database"`
|
||||
Scope string `json:"scope"`
|
||||
Address string `json:"address"`
|
||||
Commands []string `json:"commands"`
|
||||
Port int `json:"port"`
|
||||
AllowCommands bool `json:"allowCommands"`
|
||||
AllowEdit bool `json:"allowEdit"`
|
||||
AllowNew bool `json:"allowNew"`
|
||||
}
|
||||
|
||||
var (
|
||||
addr string
|
||||
config string
|
||||
database string
|
||||
scope string
|
||||
commands string
|
||||
port string
|
||||
logfile string
|
||||
plugin string
|
||||
locale string
|
||||
port int
|
||||
noAuth bool
|
||||
allowCommands bool
|
||||
allowEdit bool
|
||||
allowNew bool
|
||||
showVer bool
|
||||
version = "master"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&config, "config", "", "JSON configuration file")
|
||||
flag.StringVar(&port, "port", "0", "HTTP Port (default is random)")
|
||||
flag.StringVar(&addr, "address", "", "Address to listen to (default is all of them)")
|
||||
flag.StringVar(&database, "database", "./filemanager.db", "Database path")
|
||||
flag.StringVar(&scope, "scope", ".", "Default scope for new users")
|
||||
flag.StringVar(&commands, "commands", "git svn hg", "Space separated commands available for new users")
|
||||
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option")
|
||||
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option")
|
||||
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option")
|
||||
flag.StringVarP(&config, "config", "c", "", "Configuration file")
|
||||
flag.IntVarP(&port, "port", "p", 0, "HTTP Port (default is random)")
|
||||
flag.StringVarP(&addr, "address", "a", "", "Address to listen to (default is all of them)")
|
||||
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.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
|
||||
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users")
|
||||
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
|
||||
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users")
|
||||
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
|
||||
flag.StringVar(&locale, "locale", "en", "Default locale for new users")
|
||||
flag.StringVar(&plugin, "plugin", "", "Plugin you want to enable")
|
||||
flag.BoolVarP(&showVer, "version", "v", false, "Show version")
|
||||
}
|
||||
|
||||
func setupViper() {
|
||||
viper.SetDefault("Address", "")
|
||||
viper.SetDefault("Port", "0")
|
||||
viper.SetDefault("Database", "./filemanager.db")
|
||||
viper.SetDefault("Scope", ".")
|
||||
viper.SetDefault("Logger", "stdout")
|
||||
viper.SetDefault("Commands", []string{"git", "svn", "hg"})
|
||||
viper.SetDefault("AllowCommmands", true)
|
||||
viper.SetDefault("AllowEdit", true)
|
||||
viper.SetDefault("AllowNew", true)
|
||||
viper.SetDefault("Plugin", "")
|
||||
viper.SetDefault("Locale", "en")
|
||||
viper.SetDefault("NoAuth", false)
|
||||
|
||||
viper.BindPFlag("Port", flag.Lookup("port"))
|
||||
viper.BindPFlag("Address", flag.Lookup("address"))
|
||||
viper.BindPFlag("Database", flag.Lookup("database"))
|
||||
viper.BindPFlag("Scope", flag.Lookup("scope"))
|
||||
viper.BindPFlag("Logger", flag.Lookup("log"))
|
||||
viper.BindPFlag("Commands", flag.Lookup("commands"))
|
||||
viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
|
||||
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
|
||||
viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
|
||||
viper.BindPFlag("Locale", flag.Lookup("locale"))
|
||||
viper.BindPFlag("Plugin", flag.Lookup("plugin"))
|
||||
viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
|
||||
|
||||
viper.SetConfigName("filemanager")
|
||||
viper.AddConfigPath(".")
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupViper()
|
||||
flag.Parse()
|
||||
|
||||
if config != "" {
|
||||
loadConfig()
|
||||
if showVer {
|
||||
fmt.Println("filemanager version", version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
fm, err := filemanager.New(database, filemanager.User{
|
||||
AllowCommands: allowCommands,
|
||||
AllowEdit: allowEdit,
|
||||
AllowNew: allowNew,
|
||||
Commands: strings.Split(strings.TrimSpace(commands), " "),
|
||||
// Add a configuration file if set.
|
||||
if config != "" {
|
||||
ext := filepath.Ext(config)
|
||||
dir := filepath.Dir(config)
|
||||
config = strings.TrimSuffix(config, ext)
|
||||
|
||||
if dir != "" {
|
||||
viper.AddConfigPath(dir)
|
||||
config = strings.TrimPrefix(config, dir)
|
||||
}
|
||||
|
||||
viper.SetConfigName(config)
|
||||
}
|
||||
|
||||
// Read configuration from a file if exists.
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
if _, ok := err.(viper.ConfigParseError); ok {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up process log before anything bad happens.
|
||||
switch viper.GetString("Logger") {
|
||||
case "stdout":
|
||||
log.SetOutput(os.Stdout)
|
||||
case "stderr":
|
||||
log.SetOutput(os.Stderr)
|
||||
case "":
|
||||
log.SetOutput(ioutil.Discard)
|
||||
default:
|
||||
log.SetOutput(&lumberjack.Logger{
|
||||
Filename: logfile,
|
||||
MaxSize: 100,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 10,
|
||||
})
|
||||
}
|
||||
|
||||
// Create a File Manager instance.
|
||||
fm, err := filemanager.New(viper.GetString("Database"), filemanager.User{
|
||||
AllowCommands: viper.GetBool("AllowCommands"),
|
||||
AllowEdit: viper.GetBool("AllowEdit"),
|
||||
AllowNew: viper.GetBool("AllowNew"),
|
||||
Commands: viper.GetStringSlice("Commands"),
|
||||
Rules: []*filemanager.Rule{},
|
||||
Locale: viper.GetString("Locale"),
|
||||
CSS: "",
|
||||
FileSystem: fileutils.Dir(scope),
|
||||
FileSystem: fileutils.Dir(viper.GetString("Scope")),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if viper.GetBool("NoAuth") {
|
||||
fm.NoAuth = true
|
||||
}
|
||||
|
||||
fm.SetBaseURL("/")
|
||||
fm.SetPrefixURL("/")
|
||||
|
||||
listener, err := net.Listen("tcp", addr+":"+port)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if viper.GetString("Plugin") == "hugo" {
|
||||
// Initialize the default settings for Hugo.
|
||||
hugo := &plugins.Hugo{
|
||||
Root: viper.GetString("Scope"),
|
||||
Public: filepath.Join(viper.GetString("Scope"), "public"),
|
||||
Args: []string{},
|
||||
CleanPublic: true,
|
||||
}
|
||||
|
||||
// Try to find the Hugo executable path.
|
||||
if err = hugo.Find(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = fm.ActivatePlugin("hugo", hugo); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Builds the address and a listener.
|
||||
laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
|
||||
listener, err := net.Listen("tcp", laddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Tell the user the port in which is listening.
|
||||
fmt.Println("Listening on", listener.Addr().String())
|
||||
|
||||
// Starts the server.
|
||||
if err := http.Serve(listener, fm); err != nil {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() {
|
||||
file, err := ioutil.ReadFile(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var conf *confFile
|
||||
err = json.Unmarshal(file, &conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
database = conf.Database
|
||||
scope = conf.Scope
|
||||
addr = conf.Address
|
||||
commands = strings.Join(conf.Commands, " ")
|
||||
port = strconv.Itoa(conf.Port)
|
||||
allowNew = conf.AllowNew
|
||||
allowEdit = conf.AllowEdit
|
||||
allowCommands = conf.AllowCommands
|
||||
}
|
||||
|
||||
56
doc.go
56
doc.go
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
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
|
||||
12
download.go
12
download.go
@@ -20,14 +20,14 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// If the file isn't a directory, serve it using http.ServeFile. We display it
|
||||
// inline if it is requested.
|
||||
if !c.FI.IsDir {
|
||||
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.FI.Name)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name)
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, c.FI.Path)
|
||||
http.ServeFile(w, r, c.File.Path)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -46,10 +46,10 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Clean the slashes.
|
||||
name = fileutils.SlashClean(name)
|
||||
files = append(files, filepath.Join(c.FI.Path, name))
|
||||
files = append(files, filepath.Join(c.File.Path, name))
|
||||
}
|
||||
} else {
|
||||
files = append(files, c.FI.Path)
|
||||
files = append(files, c.File.Path)
|
||||
}
|
||||
|
||||
// If the format is true, just set it to "zip".
|
||||
@@ -93,7 +93,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Defines the file name.
|
||||
name := c.FI.Name
|
||||
name := c.File.Name
|
||||
if name == "." || name == "" {
|
||||
name = "download"
|
||||
}
|
||||
|
||||
21
file.go
21
file.go
@@ -110,7 +110,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
|
||||
func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||
// Gets the directory information using the Virtual File System of
|
||||
// the user configuration.
|
||||
f, err := c.User.FileSystem.OpenFile(c.FI.VirtualPath, os.O_RDONLY, 0)
|
||||
f, err := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -328,6 +328,8 @@ func (l listing) ApplySort() {
|
||||
sort.Sort(sort.Reverse(byName(l)))
|
||||
case "size":
|
||||
sort.Sort(sort.Reverse(bySize(l)))
|
||||
case "modified":
|
||||
sort.Sort(sort.Reverse(byModified(l)))
|
||||
default:
|
||||
// If not one of the above, do nothing
|
||||
return
|
||||
@@ -338,6 +340,8 @@ func (l listing) ApplySort() {
|
||||
sort.Sort(byName(l))
|
||||
case "size":
|
||||
sort.Sort(bySize(l))
|
||||
case "modified":
|
||||
sort.Sort(byModified(l))
|
||||
default:
|
||||
sort.Sort(byName(l))
|
||||
return
|
||||
@@ -348,6 +352,7 @@ func (l listing) ApplySort() {
|
||||
// Implement sorting for listing
|
||||
type byName listing
|
||||
type bySize listing
|
||||
type byModified listing
|
||||
|
||||
// By Name
|
||||
func (l byName) Len() int {
|
||||
@@ -392,6 +397,20 @@ func (l bySize) Less(i, j int) bool {
|
||||
return iSize < jSize
|
||||
}
|
||||
|
||||
// By Modified
|
||||
func (l byModified) Len() int {
|
||||
return len(l.Items)
|
||||
}
|
||||
|
||||
func (l byModified) Swap(i, j int) {
|
||||
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
|
||||
}
|
||||
|
||||
func (l byModified) Less(i, j int) bool {
|
||||
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
|
||||
return iModified.Sub(jModified) < 0
|
||||
}
|
||||
|
||||
var textExtensions = [...]string{
|
||||
".md", ".markdown", ".mdown", ".mmark",
|
||||
".asciidoc", ".adoc", ".ad",
|
||||
|
||||
189
filemanager.go
189
filemanager.go
@@ -1,3 +1,56 @@
|
||||
// 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 (
|
||||
@@ -6,6 +59,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -16,10 +70,15 @@ import (
|
||||
)
|
||||
|
||||
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")
|
||||
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")
|
||||
plugins = map[string]Plugin{}
|
||||
)
|
||||
|
||||
// FileManager is a file manager instance. It should be creating using the
|
||||
@@ -44,6 +103,10 @@ type FileManager struct {
|
||||
// edited directly. Use SetBaseURL.
|
||||
BaseURL string
|
||||
|
||||
// NoAuth disables the authentication. When the authentication is disabled,
|
||||
// there will only exist one user, called "admin".
|
||||
NoAuth bool
|
||||
|
||||
// The Default User needed to build the New User page.
|
||||
DefaultUser *User
|
||||
|
||||
@@ -53,8 +116,8 @@ type FileManager struct {
|
||||
// A map of events to a slice of commands.
|
||||
Commands map[string][]string
|
||||
|
||||
// The plugins that have been plugged in.
|
||||
Plugins map[string]Plugin
|
||||
// The options of the plugins that have been plugged into this instance.
|
||||
Plugins map[string]interface{}
|
||||
}
|
||||
|
||||
// Command is a command function.
|
||||
@@ -84,6 +147,9 @@ type User struct {
|
||||
// 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
|
||||
@@ -115,15 +181,32 @@ type Regexp struct {
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// Plugin is a File Manager plugin.
|
||||
type Plugin interface {
|
||||
// The JavaScript that will be injected into the main page.
|
||||
JavaScript() string
|
||||
type Plugin struct {
|
||||
JavaScript string
|
||||
CommandEvents []string
|
||||
Permissions []Permission
|
||||
Handler PluginHandler
|
||||
Options interface{}
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
Name string
|
||||
Value bool
|
||||
}
|
||||
|
||||
type PluginHandler interface {
|
||||
// If the Plugin returns (0, nil), the executation of File Manager will procced as usual.
|
||||
// Otherwise it will stop.
|
||||
BeforeAPI(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
||||
AfterAPI(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
||||
Before(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
||||
After(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
|
||||
}
|
||||
|
||||
func RegisterPlugin(name string, plugin Plugin) {
|
||||
if _, ok := plugins[name]; ok {
|
||||
panic(name + " plugin is already registred")
|
||||
}
|
||||
|
||||
plugins[name] = plugin
|
||||
}
|
||||
|
||||
// DefaultUser is used on New, when no 'base' user is provided.
|
||||
@@ -136,6 +219,7 @@ var DefaultUser = User{
|
||||
Rules: []*Rule{},
|
||||
CSS: "",
|
||||
Admin: true,
|
||||
Locale: "en",
|
||||
FileSystem: fileutils.Dir("."),
|
||||
}
|
||||
|
||||
@@ -148,8 +232,8 @@ func New(database string, base User) (*FileManager, error) {
|
||||
// map and Assets box.
|
||||
m := &FileManager{
|
||||
Users: map[string]*User{},
|
||||
Plugins: map[string]interface{}{},
|
||||
assets: rice.MustFindBox("./assets/dist"),
|
||||
Plugins: map[string]Plugin{},
|
||||
}
|
||||
|
||||
// Tries to open a database on the location provided. This
|
||||
@@ -265,41 +349,63 @@ func (m *FileManager) SetBaseURL(url string) {
|
||||
m.BaseURL = strings.TrimSuffix(url, "/")
|
||||
}
|
||||
|
||||
// RegisterPlugin registers a plugin to a File Manager instance and
|
||||
// ActivatePlugin activates a plugin to a File Manager instance and
|
||||
// loads its options from the database.
|
||||
func (m *FileManager) RegisterPlugin(name string, plugin Plugin) error {
|
||||
func (m *FileManager) ActivatePlugin(name string, options interface{}) error {
|
||||
if reflect.TypeOf(options).Kind() != reflect.Ptr {
|
||||
return errors.New("options should be a pointer to interface, not interface")
|
||||
}
|
||||
|
||||
var plugin Plugin
|
||||
|
||||
if p, ok := plugins[name]; !ok {
|
||||
plugin = p
|
||||
return errors.New(name + " plugin is not registred")
|
||||
}
|
||||
|
||||
if _, ok := m.Plugins[name]; ok {
|
||||
return errors.New("Plugin already registred")
|
||||
return errors.New(name + " plugin is already activated")
|
||||
}
|
||||
|
||||
err := m.db.Get("plugins", name, &plugin)
|
||||
if err != nil && err == storm.ErrNotFound {
|
||||
err = m.db.Set("plugins", name, plugin)
|
||||
err = m.db.Set("plugin", name, plugin)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Plugins[name] = plugin
|
||||
// Register the command event hooks.
|
||||
for _, evt := range plugin.CommandEvents {
|
||||
if _, ok := m.Commands[evt]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
m.Commands[evt] = []string{}
|
||||
}
|
||||
|
||||
err = m.db.Set("config", "commands", m.Commands)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Register the user permissions.
|
||||
for _, perm := range plugin.Permissions {
|
||||
err = m.registerPermission(perm.Name, perm.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
m.Plugins[name] = options
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterEventType registers a new event type which can be triggered using Runner
|
||||
// function.
|
||||
func (m *FileManager) RegisterEventType(name string) error {
|
||||
if _, ok := m.Commands[name]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.Commands[name] = []string{}
|
||||
return m.db.Set("config", "commands", m.Commands)
|
||||
}
|
||||
|
||||
// RegisterPermission registers a new user permission and adds it to every
|
||||
// registerPermission registers a new user permission and adds it to every
|
||||
// user with it default's 'value'. If the user is an admin, it will
|
||||
// be true.
|
||||
func (m *FileManager) RegisterPermission(name string, value bool) error {
|
||||
func (m *FileManager) registerPermission(name string, value bool) error {
|
||||
if _, ok := m.DefaultUser.Permissions[name]; ok {
|
||||
return nil
|
||||
}
|
||||
@@ -334,20 +440,25 @@ func (m *FileManager) RegisterPermission(name string, value bool) error {
|
||||
// Compatible with http.Handler.
|
||||
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
code, err := serveHTTP(&RequestContext{
|
||||
FM: m,
|
||||
User: nil,
|
||||
FI: nil,
|
||||
FileManager: m,
|
||||
User: nil,
|
||||
File: nil,
|
||||
}, w, r)
|
||||
|
||||
if code != 0 {
|
||||
if code >= 400 {
|
||||
w.WriteHeader(code)
|
||||
|
||||
if err != nil {
|
||||
w.Write([]byte(err.Error()))
|
||||
} else {
|
||||
w.Write([]byte(http.StatusText(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()))
|
||||
}
|
||||
}
|
||||
|
||||
// Allowed checks if the user has permission to access a directory/file.
|
||||
|
||||
51
http.go
51
http.go
@@ -10,9 +10,9 @@ import (
|
||||
|
||||
// RequestContext contains the needed information to make handlers work.
|
||||
type RequestContext struct {
|
||||
*FileManager
|
||||
User *User
|
||||
FM *FileManager
|
||||
FI *file
|
||||
File *file
|
||||
// On API handlers, Router is the APi handler we want.
|
||||
Router string
|
||||
}
|
||||
@@ -21,9 +21,9 @@ type RequestContext struct {
|
||||
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Checks if the URL contains the baseURL and strips it. Otherwise, it just
|
||||
// returns a 404 error because we're not supposed to be here!
|
||||
p := strings.TrimPrefix(r.URL.Path, c.FM.BaseURL)
|
||||
p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
|
||||
|
||||
if len(p) >= len(r.URL.Path) && c.FM.BaseURL != "" {
|
||||
if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
if r.URL.Path == "/sw.js" {
|
||||
return renderFile(
|
||||
w,
|
||||
c.FM.assets.MustString("sw.js"),
|
||||
c.assets.MustString("sw.js"),
|
||||
"application/javascript",
|
||||
c,
|
||||
)
|
||||
@@ -65,7 +65,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
|
||||
return renderFile(
|
||||
w,
|
||||
c.FM.assets.MustString("index.html"),
|
||||
c.assets.MustString("index.html"),
|
||||
"text/html",
|
||||
c,
|
||||
)
|
||||
@@ -74,13 +74,13 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
// staticHandler handles the static assets path.
|
||||
func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if r.URL.Path != "/static/manifest.json" {
|
||||
http.FileServer(c.FM.assets.HTTPBox()).ServeHTTP(w, r)
|
||||
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return renderFile(
|
||||
w,
|
||||
c.FM.assets.MustString("static/manifest.json"),
|
||||
c.assets.MustString("static/manifest.json"),
|
||||
"application/json",
|
||||
c,
|
||||
)
|
||||
@@ -107,8 +107,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
for _, p := range c.FM.Plugins {
|
||||
code, err := p.BeforeAPI(c, w, r)
|
||||
for p := range c.Plugins {
|
||||
code, err := plugins[p].Handler.Before(c, w, r)
|
||||
if code != 0 || err != nil {
|
||||
return code, err
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
|
||||
if c.Router == "checksum" || c.Router == "download" {
|
||||
var err error
|
||||
c.FI, err = getInfo(r.URL, c.FM, c.User)
|
||||
c.File, err = getInfo(r.URL, c.FileManager, c.User)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
}
|
||||
@@ -138,10 +138,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
code, err = resourceHandler(c, w, r)
|
||||
case "users":
|
||||
code, err = usersHandler(c, w, r)
|
||||
case "commands":
|
||||
code, err = commandsHandler(c, w, r)
|
||||
case "plugins":
|
||||
code, err = pluginsHandler(c, w, r)
|
||||
case "settings":
|
||||
code, err = settingsHandler(c, w, r)
|
||||
default:
|
||||
code = http.StatusNotFound
|
||||
}
|
||||
@@ -150,8 +148,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
return code, err
|
||||
}
|
||||
|
||||
for _, p := range c.FM.Plugins {
|
||||
code, err := p.AfterAPI(c, w, r)
|
||||
for p := range c.Plugins {
|
||||
code, err := plugins[p].Handler.After(c, w, r)
|
||||
if code != 0 || err != nil {
|
||||
return code, err
|
||||
}
|
||||
@@ -164,7 +162,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
|
||||
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
query := r.URL.Query().Get("algo")
|
||||
|
||||
val, err := c.FI.Checksum(query)
|
||||
val, err := c.File.Checksum(query)
|
||||
if err == errInvalidOption {
|
||||
return http.StatusBadRequest, err
|
||||
} else if err != nil {
|
||||
@@ -194,18 +192,17 @@ func splitURL(path string) (string, string) {
|
||||
|
||||
// renderFile renders a file using a template with some needed variables.
|
||||
func renderFile(w http.ResponseWriter, file string, contentType string, c *RequestContext) (int, error) {
|
||||
functions := template.FuncMap{
|
||||
"JS": func(s string) template.JS {
|
||||
return template.JS(s)
|
||||
},
|
||||
}
|
||||
|
||||
tpl := template.Must(template.New("file").Funcs(functions).Parse(file))
|
||||
tpl := template.Must(template.New("file").Parse(file))
|
||||
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
||||
|
||||
var javascript = ""
|
||||
for name := range c.Plugins {
|
||||
javascript += plugins[name].JavaScript + "\n"
|
||||
}
|
||||
|
||||
err := tpl.Execute(w, map[string]interface{}{
|
||||
"BaseURL": c.FM.RootURL(),
|
||||
"Plugins": c.FM.Plugins,
|
||||
"BaseURL": c.RootURL(),
|
||||
"JavaScript": template.JS(javascript),
|
||||
})
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
|
||||
34
package.json
34
package.json
@@ -14,53 +14,57 @@
|
||||
"moment": "^2.18.1",
|
||||
"normalize.css": "^7.0.0",
|
||||
"vue": "^2.3.3",
|
||||
"vue-i18n": "^7.1.0",
|
||||
"vue-router": "^2.7.0",
|
||||
"vuex": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.7.2",
|
||||
"autoprefixer": "^7.1.2",
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^6.2.10",
|
||||
"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": "^1.1.3",
|
||||
"chalk": "^2.0.1",
|
||||
"connect-history-api-fallback": "^1.3.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-standard": "^6.2.1",
|
||||
"eslint-friendly-formatter": "^2.0.7",
|
||||
"eslint": "^4.3.0",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-friendly-formatter": "^3.0.0",
|
||||
"eslint-loader": "^1.7.1",
|
||||
"eslint-plugin-html": "^2.0.0",
|
||||
"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-plugin-standard": "^2.0.1",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.14.1",
|
||||
"extract-text-webpack-plugin": "^2.0.0",
|
||||
"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",
|
||||
"opn": "^4.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "^1.3.0",
|
||||
"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",
|
||||
"sw-precache-webpack-plugin": "^0.9.1",
|
||||
"sw-precache-webpack-plugin": "^0.11.4",
|
||||
"uglify-js": "^3.0.23",
|
||||
"url-loader": "^0.5.8",
|
||||
"vue-loader": "^12.1.0",
|
||||
"vue-loader": "^13.0.2",
|
||||
"vue-style-loader": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.3",
|
||||
"webpack": "^2.6.1",
|
||||
"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"
|
||||
"webpack-merge": "^4.1.0",
|
||||
"yml-loader": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
|
||||
228
plugins/hugo.go
Normal file
228
plugins/hugo.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hacdias/filemanager"
|
||||
"github.com/hacdias/varutils"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
func init() {
|
||||
filemanager.RegisterPlugin("hugo", filemanager.Plugin{
|
||||
JavaScript: hugoJavaScript,
|
||||
CommandEvents: []string{"before_publish", "after_publish"},
|
||||
Permissions: []filemanager.Permission{
|
||||
{
|
||||
Name: "allowPublish",
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
Handler: &hugo{},
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
ErrHugoNotFound = errors.New("It seems that tou don't have 'hugo' on your PATH")
|
||||
ErrUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
|
||||
)
|
||||
|
||||
// Hugo is a hugo (https://gohugo.io) plugin.
|
||||
type Hugo struct {
|
||||
// Website root
|
||||
Root string `name:"Website Root"`
|
||||
// Public folder
|
||||
Public string `name:"Public Directory"`
|
||||
// Hugo executable path
|
||||
Exe string `name:"Hugo Executable"`
|
||||
// Hugo arguments
|
||||
Args []string `name:"Hugo Arguments"`
|
||||
// Indicates if we should clean public before a new publish.
|
||||
CleanPublic bool `name:"Clean Public"`
|
||||
}
|
||||
|
||||
// Find finds the hugo executable in the path.
|
||||
func (h *Hugo) Find() error {
|
||||
var err error
|
||||
if h.Exe, err = exec.LookPath("hugo"); err != nil {
|
||||
return ErrHugoNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// run runs Hugo with the define arguments.
|
||||
func (h Hugo) run(force bool) {
|
||||
// If the CleanPublic option is enabled, clean it.
|
||||
if h.CleanPublic {
|
||||
os.RemoveAll(h.Public)
|
||||
}
|
||||
|
||||
// Prevent running if watching is enabled
|
||||
if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force {
|
||||
if len(h.Args) > pos && h.Args[pos+1] != "false" {
|
||||
return
|
||||
}
|
||||
|
||||
if len(h.Args) == pos+1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := Run(h.Exe, h.Args, h.Root); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// schedule schedules a post to be published later.
|
||||
func (h Hugo) schedule(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t, err := time.Parse("2006-01-02T15:04", r.Header.Get("Schedule"))
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
path = filepath.Clean(path)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
scheduler := cron.New()
|
||||
scheduler.AddFunc(t.Format("05 04 15 02 01 *"), func() {
|
||||
if err := h.undraft(path); err != nil {
|
||||
log.Printf(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.run(false)
|
||||
})
|
||||
|
||||
scheduler.Start()
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (h Hugo) undraft(file string) error {
|
||||
args := []string{"undraft", file}
|
||||
if err := Run(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type hugo struct{}
|
||||
|
||||
func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
o := c.Plugins["hugo"].(*Hugo)
|
||||
|
||||
// If we are using the 'magic url' for the settings, we should redirect the
|
||||
// request for the acutual path.
|
||||
if r.URL.Path == "/settings/" || r.URL.Path == "/settings" {
|
||||
var frontmatter string
|
||||
var err error
|
||||
|
||||
if _, err = os.Stat(filepath.Join(o.Root, "config.yaml")); err == nil {
|
||||
frontmatter = "yaml"
|
||||
}
|
||||
|
||||
if _, err = os.Stat(filepath.Join(o.Root, "config.json")); err == nil {
|
||||
frontmatter = "json"
|
||||
}
|
||||
|
||||
if _, err = os.Stat(filepath.Join(o.Root, "config.toml")); err == nil {
|
||||
frontmatter = "toml"
|
||||
}
|
||||
|
||||
r.URL.Path = "/config." + frontmatter
|
||||
}
|
||||
|
||||
// From here on, we only care about 'hugo' router so we can bypass
|
||||
// the others.
|
||||
if c.Router != "hugo" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// If we are not using HTTP Post, we shall return Method Not Allowed
|
||||
// since we are only working with this method.
|
||||
if r.Method != http.MethodPost {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
// If we are creating a file built from an archetype.
|
||||
if r.Header.Get("Archetype") != "" {
|
||||
if !c.User.AllowNew {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
archetype := r.Header.Get("archetype")
|
||||
|
||||
ext := filepath.Ext(filename)
|
||||
|
||||
// If the request isn't for a markdown file, we can't
|
||||
// handle it.
|
||||
if ext != ".markdown" && ext != ".md" {
|
||||
return http.StatusBadRequest, ErrUnsupportedFileType
|
||||
}
|
||||
|
||||
// Tries to create a new file based on this archetype.
|
||||
args := []string{"new", filename, "--kind", archetype}
|
||||
if err := Run(o.Exe, args, o.Root); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Writes the location of the new file to the Header.
|
||||
w.Header().Set("Location", "/files/content/"+filename)
|
||||
return http.StatusCreated, nil
|
||||
}
|
||||
|
||||
// If we are trying to regenerate the website.
|
||||
if r.Header.Get("Regenerate") == "true" {
|
||||
if !c.User.Permissions["allowPublish"] {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
|
||||
// Before save command handler.
|
||||
if err := c.Runner("before_publish", filename); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// We only run undraft command if it is a file.
|
||||
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
|
||||
if err := o.undraft(filename); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Regenerates the file
|
||||
o.run(false)
|
||||
|
||||
// Executed the before publish command.
|
||||
if err := c.Runner("before_publish", filename); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
if r.Header.Get("Schedule") != "" {
|
||||
if !c.User.Permissions["allowPublish"] {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
return o.schedule(c, w, r)
|
||||
}
|
||||
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
func (h hugo) After(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
'use strict';
|
||||
package plugins
|
||||
|
||||
const hugoJavaScript = `'use strict';
|
||||
|
||||
(function () {
|
||||
if (window.plugins === undefined || window.plugins === null) {
|
||||
@@ -10,8 +12,8 @@
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${data.store.state.baseURL}/api/hugo${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`)
|
||||
request.open('POST', data.store.state.baseURL + "/api/hugo" + url, true)
|
||||
request.setRequestHeader('Authorization', "Bearer " + data.store.state.jwt)
|
||||
request.setRequestHeader('Regenerate', 'true')
|
||||
|
||||
request.onload = () => {
|
||||
@@ -32,8 +34,8 @@
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${data.store.state.baseURL}/api/hugo${url}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`)
|
||||
request.open('POST', data.store.state.baseURL + "/api/hugo" + url, true)
|
||||
request.setRequestHeader('Authorization',"Bearer " + data.store.state.jwt)
|
||||
request.setRequestHeader('Archetype', encodeURIComponent(type))
|
||||
|
||||
request.onload = () => {
|
||||
@@ -54,8 +56,8 @@
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new window.XMLHttpRequest()
|
||||
request.open('POST', `${data.store.state.baseURL}/api/hugo${file}`, true)
|
||||
request.setRequestHeader('Authorization', `Bearer ${data.store.state.jwt}`)
|
||||
request.open('POST', data.store.state.baseURL + "/api/hugo" + file, true)
|
||||
request.setRequestHeader('Authorization', "Bearer " + data.store.state.jwt)
|
||||
request.setRequestHeader('Schedule', date)
|
||||
|
||||
request.onload = () => {
|
||||
@@ -80,8 +82,6 @@
|
||||
if: function (data, route) {
|
||||
return (data.store.state.req.kind === 'editor' &&
|
||||
!data.store.state.loading &&
|
||||
data.store.state.req.metadata !== undefined &&
|
||||
data.store.state.req.metadata !== null &&
|
||||
data.store.state.user.allowEdit &
|
||||
data.store.state.user.permissions.allowPublish)
|
||||
},
|
||||
@@ -224,4 +224,4 @@
|
||||
}
|
||||
]
|
||||
})
|
||||
})()
|
||||
})()`
|
||||
@@ -1,4 +1,4 @@
|
||||
package hugo
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
12
resource.go
12
resource.go
@@ -34,7 +34,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
case http.MethodPut:
|
||||
// Before save command handler.
|
||||
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
|
||||
if err := c.FM.Runner("before_save", path); err != nil {
|
||||
if err := c.Runner("before_save", path); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// After save command handler.
|
||||
if err := c.FM.Runner("after_save", path); err != nil {
|
||||
if err := c.Runner("after_save", path); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Gets the information of the directory/file.
|
||||
f, err := getInfo(r.URL, c.FM, c.User)
|
||||
f, err := getInfo(r.URL, c.FileManager, c.User)
|
||||
if err != nil {
|
||||
return errorToHTTP(err, false), err
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// If it is a dir, go and serve the listing.
|
||||
if f.IsDir {
|
||||
c.FI = f
|
||||
c.File = f
|
||||
return listingHandler(c, w, r)
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
f := c.FI
|
||||
f := c.File
|
||||
f.Kind = "listing"
|
||||
|
||||
// Tries to get the listing data.
|
||||
@@ -112,7 +112,7 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
|
||||
listing := f.listing
|
||||
|
||||
// Defines the cookie scope.
|
||||
cookieScope := c.FM.RootURL()
|
||||
cookieScope := c.RootURL()
|
||||
if cookieScope == "" {
|
||||
cookieScope = "/"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
8868d0da70d74a9255e7fd2617e88a63ba160479
|
||||
7327806da8feadd5f82e2286efb2e2dd44109d3e
|
||||
135
settings.go
135
settings.go
@@ -2,103 +2,128 @@ package filemanager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
func commandsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
return commandsGetHandler(c, w, r)
|
||||
case http.MethodPut:
|
||||
return commandsPutHandler(c, w, r)
|
||||
}
|
||||
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
type modifySettingsRequest struct {
|
||||
*modifyRequest
|
||||
Data struct {
|
||||
Commands map[string][]string `json:"commands"`
|
||||
Plugins map[string]map[string]interface{} `json:"plugins"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func commandsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
return renderJSON(w, c.FM.Commands)
|
||||
type pluginOption struct {
|
||||
Variable string `json:"variable"`
|
||||
Name string `json:"name"`
|
||||
Value interface{} `json:"value"`
|
||||
}
|
||||
|
||||
func commandsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
|
||||
// Checks if the request body is empty.
|
||||
if r.Body == nil {
|
||||
return http.StatusBadGateway, errors.New("Empty request body")
|
||||
return nil, errEmptyRequest
|
||||
}
|
||||
|
||||
var commands map[string][]string
|
||||
|
||||
// Parses the user and checks for error.
|
||||
err := json.NewDecoder(r.Body).Decode(&commands)
|
||||
// Parses the request body and checks if it's well formed.
|
||||
mod := &modifySettingsRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(mod)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, errors.New("Invalid JSON")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.FM.db.Set("config", "commands", commands); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
// Checks if the request type is right.
|
||||
if mod.What != "settings" {
|
||||
return nil, errWrongDataType
|
||||
}
|
||||
|
||||
c.FM.Commands = commands
|
||||
return http.StatusOK, nil
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
func pluginsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if r.URL.Path != "" && r.URL.Path != "/" {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
return pluginsGetHandler(c, w, r)
|
||||
return settingsGetHandler(c, w, r)
|
||||
case http.MethodPut:
|
||||
return pluginsPutHandler(c, w, r)
|
||||
return settingsPutHandler(c, w, r)
|
||||
}
|
||||
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
func pluginsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
return renderJSON(w, c.FM.Plugins)
|
||||
type settingsGetRequest struct {
|
||||
Commands map[string][]string `json:"commands"`
|
||||
Plugins map[string][]pluginOption `json:"plugins"`
|
||||
}
|
||||
|
||||
func pluginsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
if r.Body == nil {
|
||||
return http.StatusBadGateway, errors.New("Empty request body")
|
||||
result := &settingsGetRequest{
|
||||
Commands: c.Commands,
|
||||
Plugins: map[string][]pluginOption{},
|
||||
}
|
||||
|
||||
var raw map[string]map[string]interface{}
|
||||
for name, p := range c.Plugins {
|
||||
result.Plugins[name] = []pluginOption{}
|
||||
|
||||
// Parses the user and checks for error.
|
||||
err := json.NewDecoder(r.Body).Decode(&raw)
|
||||
t := reflect.TypeOf(p).Elem()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
result.Plugins[name] = append(result.Plugins[name], pluginOption{
|
||||
Variable: t.Field(i).Name,
|
||||
Name: t.Field(i).Tag.Get("name"),
|
||||
Value: reflect.ValueOf(p).Elem().FieldByName(t.Field(i).Name).Interface(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return renderJSON(w, result)
|
||||
}
|
||||
|
||||
func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if !c.User.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
mod, err := parsePutSettingsRequest(r)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
for name, plugin := range raw {
|
||||
err = mapstructure.Decode(plugin, c.FM.Plugins[name])
|
||||
if err != nil {
|
||||
// Update the commands.
|
||||
if mod.Which == "commands" {
|
||||
if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
err = c.FM.db.Set("plugins", name, c.FM.Plugins[name])
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
c.Commands = mod.Data.Commands
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
// Update the plugins.
|
||||
if mod.Which == "plugins" {
|
||||
for name, plugin := range mod.Data.Plugins {
|
||||
err = mapstructure.Decode(plugin, c.Plugins[name])
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
err = c.db.Set("plugins", name, c.Plugins[name])
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
214
users.go
214
users.go
@@ -11,20 +11,22 @@ import (
|
||||
"github.com/asdine/storm"
|
||||
)
|
||||
|
||||
type modifyRequest struct {
|
||||
What string `json:"what"` // Answer to: what data type?
|
||||
Which string `json:"which"` // Answer to: which field?
|
||||
}
|
||||
|
||||
type modifyUserRequest struct {
|
||||
*modifyRequest
|
||||
Data *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) {
|
||||
if r.URL.Path == "/change-password" {
|
||||
return usersUpdatePassword(c, w, r)
|
||||
}
|
||||
|
||||
if r.URL.Path == "/change-css" {
|
||||
return usersUpdateCSS(c, w, r)
|
||||
}
|
||||
|
||||
// If the user is admin and the HTTP Method is not
|
||||
// PUT, then we return forbidden.
|
||||
if !c.User.Admin {
|
||||
// If the user isn't admin and isn't making a PUT
|
||||
// request, then return forbidden.
|
||||
if !c.User.Admin && r.Method != http.MethodPut {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
@@ -61,32 +63,38 @@ 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, error) {
|
||||
func getUser(r *http.Request) (*User, string, error) {
|
||||
// Checks if the request body is empty.
|
||||
if r.Body == nil {
|
||||
return nil, errEmptyRequest
|
||||
return nil, "", errEmptyRequest
|
||||
}
|
||||
|
||||
u := &User{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(u)
|
||||
// Parses the request body and checks if it's well formed.
|
||||
mod := &modifyUserRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(mod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
// Checks if the request type is right.
|
||||
if mod.What != "user" {
|
||||
return nil, "", errWrongDataType
|
||||
}
|
||||
|
||||
return mod.Data, mod.Which, nil
|
||||
}
|
||||
|
||||
func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Request for the default user data.
|
||||
if r.URL.Path == "/base" {
|
||||
return renderJSON(w, c.FM.DefaultUser)
|
||||
return renderJSON(w, c.DefaultUser)
|
||||
}
|
||||
|
||||
// Request for the listing of users.
|
||||
if r.URL.Path == "/" {
|
||||
users := []User{}
|
||||
|
||||
for _, user := range c.FM.Users {
|
||||
for _, user := range c.Users {
|
||||
// Copies the user info and removes its
|
||||
// password so it won't be sent to the
|
||||
// front-end.
|
||||
@@ -108,7 +116,7 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Searches for the user and prints the one who matches.
|
||||
for _, user := range c.FM.Users {
|
||||
for _, user := range c.Users {
|
||||
if user.ID != id {
|
||||
continue
|
||||
}
|
||||
@@ -127,11 +135,26 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
u, err := getUser(r)
|
||||
u, _, err := getUser(r)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// Checks if username isn't empty.
|
||||
if u.Username == "" {
|
||||
return http.StatusBadRequest, errEmptyUsername
|
||||
}
|
||||
|
||||
// Checks if filesystem isn't empty.
|
||||
if u.FileSystem == "" {
|
||||
return http.StatusBadRequest, 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")
|
||||
@@ -161,7 +184,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
u.Password = pw
|
||||
|
||||
// Saves the user to the database.
|
||||
err = c.FM.db.Save(u)
|
||||
err = c.db.Save(u)
|
||||
if err == storm.ErrAlreadyExists {
|
||||
return http.StatusConflict, errUserExist
|
||||
}
|
||||
@@ -171,7 +194,7 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Saves the user to the memory.
|
||||
c.FM.Users[u.Username] = u
|
||||
c.Users[u.Username] = u
|
||||
|
||||
// Set the Location header and return.
|
||||
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
|
||||
@@ -190,7 +213,7 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// Deletes the user from the database.
|
||||
err = c.FM.db.DeleteStruct(&User{ID: id})
|
||||
err = c.db.DeleteStruct(&User{ID: id})
|
||||
if err == storm.ErrNotFound {
|
||||
return http.StatusNotFound, errUserNotExist
|
||||
}
|
||||
@@ -200,9 +223,9 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// Delete the user from the in-memory users map.
|
||||
for _, user := range c.FM.Users {
|
||||
for _, user := range c.Users {
|
||||
if user.ID == id {
|
||||
delete(c.FM.Users, user.Username)
|
||||
delete(c.Users, user.Username)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -210,72 +233,79 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func usersUpdatePassword(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if r.Method != http.MethodPut {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
u, err := getUser(r)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
if u.Password == "" {
|
||||
return http.StatusBadRequest, errEmptyPassword
|
||||
}
|
||||
|
||||
pw, err := hashPassword(u.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
c.User.Password = pw
|
||||
err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func usersUpdateCSS(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
if r.Method != http.MethodPut {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
u, err := getUser(r)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
c.User.CSS = u.CSS
|
||||
err = c.FM.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// New users should be created on /api/users.
|
||||
if r.URL.Path == "/" {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
// Gets the user ID from the URL and checks if it's valid.
|
||||
id, err := getUserID(r)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
u, err := getUser(r)
|
||||
// Checks if the user has permission to access this page.
|
||||
if !c.User.Admin && id != c.User.ID {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Gets the user from the request body.
|
||||
u, which, err := getUser(r)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// The username and the filesystem cannot be empty.
|
||||
if u.Username == "" || u.FileSystem == "" {
|
||||
return http.StatusBadRequest, errors.New("Username, password or scope are empty")
|
||||
// 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
|
||||
}
|
||||
|
||||
err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// Updates the Password.
|
||||
if which == "password" {
|
||||
if u.Password == "" {
|
||||
return http.StatusBadRequest, errEmptyPassword
|
||||
}
|
||||
|
||||
pw, err := 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)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// If can only be all.
|
||||
if which != "all" {
|
||||
return http.StatusBadRequest, errInvalidUpdateField
|
||||
}
|
||||
|
||||
// Checks if username isn't empty.
|
||||
if u.Username == "" {
|
||||
return http.StatusBadRequest, errEmptyUsername
|
||||
}
|
||||
|
||||
// Checks if filesystem isn't empty.
|
||||
if u.FileSystem == "" {
|
||||
return http.StatusBadRequest, errEmptyScope
|
||||
}
|
||||
|
||||
// Initialize rules if they're not initialized.
|
||||
@@ -288,48 +318,50 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
|
||||
u.Commands = []string{}
|
||||
}
|
||||
|
||||
var ouser *User
|
||||
for _, user := range c.FM.Users {
|
||||
// Gets the current saved user from the in-memory map.
|
||||
var suser *User
|
||||
for _, user := range c.Users {
|
||||
if user.ID == id {
|
||||
ouser = user
|
||||
suser = user
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ouser == nil {
|
||||
if suser == nil {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
|
||||
u.ID = id
|
||||
|
||||
if u.Password == "" {
|
||||
u.Password = ouser.Password
|
||||
} else {
|
||||
// Changes the password if the request wants it.
|
||||
if u.Password != "" {
|
||||
pw, err := hashPassword(u.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
u.Password = pw
|
||||
} else {
|
||||
u.Password = suser.Password
|
||||
}
|
||||
|
||||
// Default permissions if current are nil.
|
||||
if u.Permissions == nil {
|
||||
u.Permissions = c.FM.DefaultUser.Permissions
|
||||
u.Permissions = c.DefaultUser.Permissions
|
||||
}
|
||||
|
||||
// Updates the whole User struct because we always are supposed
|
||||
// to send a new entire object.
|
||||
err = c.FM.db.Save(u)
|
||||
err = c.db.Save(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 ouser.Username != u.Username {
|
||||
delete(c.FM.Users, ouser.Username)
|
||||
if suser.Username != u.Username {
|
||||
delete(c.Users, suser.Username)
|
||||
}
|
||||
|
||||
c.FM.Users[u.Username] = u
|
||||
c.Users[u.Username] = u
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user