Internationalization (#183)

* update dependencies to latest version

* add mising dependencies

* Syntax updates and such

* Reorganize files and translate login to portuguese

* Add i18n to buttons

* Error translations and some bug fixes

* Add i18n to files

* i18n on prompts

* update search

* Prompts and Sidebar in

* i18n to the header

* Change to YAML

* alphabetical order

* # Add simplified Chinese language (#180)

* Add Simplified Chinese and sort by alphabet

* Add more text to translations

* API Updates

* Update zh_cn.yaml (#182)

* Api Upgrades

* Simplify api and clean zh_cn lang file

* Improve error logging

* Fix some route bugs and separate login styles

* better organization

* Fix bug on api

* Build assets Tue, Aug  1, 2017 11:32:23 AM

* Rename users path and fix bug scroll event

* Start Portuguese translation and file org

* Add more to the PT translation

* Add show

* Build assets Tue Aug  1 12:01:39 GMTST 2017

* Add locale to cofnig

* Update portuguese translation

* You can change the language :)

* :D

* Build assets Tue Aug  1 17:50:31 GMTST 2017

* Update requestContext variable names

* Remove assets

* Build assets Tue Aug  1 20:48:21 GMTST 2017


Former-commit-id: 08f373725c14990f61dbb00bea43118c496c5d32 [formerly 281e23007c79dac1e9b86424201891a99d20f73a] [formerly b1b73f42debbce06b4f36e4cf97e319789c85b9f [formerly d8bc73390c]]
Former-commit-id: 92e99405cbf9935d1cf77b0fe70b122fca552be6 [formerly 3cd365e862f2a54ada60e226a19ac607b8d0c43b]
Former-commit-id: cf9815114ac686cdf75a6b1cba15adafe493d083
pull/726/head
Henrique Dias 2017-08-01 20:49:56 +01:00 committed by GitHub
parent a5a68a8944
commit d50bec8caa
67 changed files with 1450 additions and 887 deletions

View File

@ -25,6 +25,10 @@ module.exports = {
},
module: {
rules: [
{
test: /\.(yml|yaml)$/,
loader: 'yml-loader'
},
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',

View File

@ -21,11 +21,10 @@
<!-- 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>{{ .JavaScript }}</script>

View File

@ -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 {

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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')
}
}
},

View File

@ -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>
@ -25,21 +25,21 @@
</div>
<div>
<router-link class="action" to="/settings" aria-label="Settings" title="Settings">
<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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,23 +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: sizeSorted }" class="size" @click="sort('size')">
<span>Size</span>
<span>{{ $t('files.size') }}</span>
<i class="material-icons">{{ sizeIcon }}</i>
</p>
<p :class="{ active: modifiedSorted }" class="modified" @click="sort('modified')">
<span>Last modified</span>
<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"
@ -47,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"
@ -62,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>

View File

@ -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',

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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;
}

View File

@ -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
View 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
View 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
View 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)

151
assets/src/i18n/zh-cn.yaml Normal file
View File

@ -0,0 +1,151 @@
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: 全局设置
newPassword: 您的新密码
newPasswordConfirm: 重输一遍新密码
newUser: 新建用户
password: 密码
passwordUpdated: 密码更新!
permissions: 权限
permissionsHelp: >
'您可以将该用户设置为管理员 或单独选择各项权限. 如果选择 "管理员(Administrator)" ,
将自动检查所有其他选项, 并且该用户可以管理其他用户.'
pluginsUpdated: 插件设置更新!
profileSettings: 配置文件设置
ruleExample1: >
'阻止用户访问每个文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore).'
ruleExample2: 阻止用户访问其目录范围内任何名为 Caddyfile 的文件/文件夹.
rules: 规则
rulesHelp1: >
'这里您可以为特定用户制定一组允许或不允许的规则,
阻止的文件将不会显示到列表中, 用户将无法访问, 支持相对于用户的范围.'
rulesHelp2: >
每行一条规则, 必须以关键词 {0} 或 {1} 开头. 如果使用正则表达式,
然后使用表达式或路径, 则需要在第二列单词加入 {2} .
scope: 目录范围
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 键(回车)执行.

View File

@ -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 }
})

View File

@ -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',

View File

@ -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)),

View File

@ -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
}

View File

@ -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))
})
}

View File

@ -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,11 @@ 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', {})
@ -152,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!
@ -220,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')
},

View File

@ -1,12 +1,20 @@
<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>
@ -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,24 +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 => {
for (let key in plugins) {
this.plugins.push(this.parsePlugin(key, plugins[key]))
}
})
.catch(error => { this.showError(error) })
},
methods: {
...mapMutations([ 'showSuccess', 'showError' ]),
@ -102,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) {
@ -129,10 +131,8 @@ export default {
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) {

View File

@ -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,

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -27,7 +27,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
}
@ -78,7 +78,7 @@ 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)
string, err := token.SignedString(c.key)
if err != nil {
return http.StatusInternalServerError, err
@ -114,7 +114,7 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
// User if it is valid.
func validateAuth(c *RequestContext, r *http.Request) (bool, *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,
@ -127,7 +127,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
}

View File

@ -28,6 +28,7 @@ var (
commands string
logfile string
plugin string
locale string
port int
allowCommands bool
allowEdit bool
@ -47,6 +48,7 @@ func init() {
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.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")
}
@ -62,6 +64,7 @@ func setupViper() {
viper.SetDefault("AllowEdit", true)
viper.SetDefault("AllowNew", true)
viper.SetDefault("Plugin", "")
viper.SetDefault("Locale", "en")
viper.BindPFlag("Port", flag.Lookup("port"))
viper.BindPFlag("Address", flag.Lookup("address"))
@ -72,6 +75,7 @@ func setupViper() {
viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
viper.BindPFlag("Locale", flag.Lookup("locale"))
viper.BindPFlag("Plugin", flag.Lookup("plugin"))
viper.SetConfigName("filemanager")
@ -133,6 +137,7 @@ func main() {
AllowNew: viper.GetBool("AllowNew"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
FileSystem: fileutils.Dir(viper.GetString("Scope")),
})

View File

@ -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"
}

View File

@ -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
}

View File

@ -70,11 +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")
plugins = map[string]Plugin{}
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
@ -139,6 +143,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
@ -208,6 +215,7 @@ var DefaultUser = User{
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
FileSystem: fileutils.Dir("."),
}
@ -428,23 +436,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 {
log.Print(err)
w.Write([]byte(err.Error()))
} else {
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.

34
http.go
View File

@ -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,7 +107,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return http.StatusForbidden, nil
}
for p := range c.FM.Plugins {
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,7 +148,7 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return code, err
}
for p := range c.FM.Plugins {
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 {
@ -198,12 +196,12 @@ func renderFile(w http.ResponseWriter, file string, contentType string, c *Reque
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
var javascript = ""
for name := range c.FM.Plugins {
for name := range c.Plugins {
javascript += plugins[name].JavaScript + "\n"
}
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.FM.RootURL(),
"BaseURL": c.RootURL(),
"JavaScript": template.JS(javascript),
})
if err != nil {

View File

@ -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",

View File

@ -117,7 +117,7 @@ func (h Hugo) undraft(file string) error {
type hugo struct{}
func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
o := c.FM.Plugins["hugo"].(*Hugo)
o := c.Plugins["hugo"].(*Hugo)
// If we are using the 'magic url' for the settings, we should redirect the
// request for the acutual path.
@ -189,7 +189,7 @@ func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *ht
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// Before save command handler.
if err := c.FM.Runner("before_publish", filename); err != nil {
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}
@ -205,7 +205,7 @@ func (h hugo) Before(c *filemanager.RequestContext, w http.ResponseWriter, r *ht
o.run(false)
// Executed the before publish command.
if err := c.FM.Runner("before_publish", filename); err != nil {
if err := c.Runner("before_publish", filename); err != nil {
return http.StatusInternalServerError, err
}

View File

@ -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 = "/"
}

View File

@ -1 +1 @@
49dd472ced00d5e02963554cd0b84393f8c08d75
b5a8f3badeeb5ea5e285f23298ddef20ce247376

View File

@ -2,66 +2,18 @@ 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
}
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)
}
func commandsPutHandler(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")
}
var commands map[string][]string
// Parses the user and checks for error.
err := json.NewDecoder(r.Body).Decode(&commands)
if err != nil {
return http.StatusBadRequest, errors.New("Invalid JSON")
}
if err := c.FM.db.Set("config", "commands", commands); err != nil {
return http.StatusInternalServerError, err
}
c.FM.Commands = commands
return http.StatusOK, nil
}
func pluginsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
switch r.Method {
case http.MethodGet:
return pluginsGetHandler(c, w, r)
case http.MethodPut:
return pluginsPutHandler(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"`
}
type pluginOption struct {
@ -70,19 +22,63 @@ type pluginOption struct {
Value interface{} `json:"value"`
}
func pluginsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request body is empty.
if r.Body == nil {
return nil, errEmptyRequest
}
// Parses the request body and checks if it's well formed.
mod := &modifySettingsRequest{}
err := json.NewDecoder(r.Body).Decode(mod)
if err != nil {
return nil, err
}
// Checks if the request type is right.
if mod.What != "settings" {
return nil, errWrongDataType
}
return mod, nil
}
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 settingsGetHandler(c, w, r)
case http.MethodPut:
return settingsPutHandler(c, w, r)
}
return http.StatusMethodNotAllowed, nil
}
type settingsGetRequest struct {
Commands map[string][]string `json:"commands"`
Plugins map[string][]pluginOption `json:"plugins"`
}
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin {
return http.StatusForbidden, nil
}
plugins := map[string][]pluginOption{}
result := &settingsGetRequest{
Commands: c.Commands,
Plugins: map[string][]pluginOption{},
}
for name, p := range c.FM.Plugins {
plugins[name] = []pluginOption{}
for name, p := range c.Plugins {
result.Plugins[name] = []pluginOption{}
t := reflect.TypeOf(p).Elem()
for i := 0; i < t.NumField(); i++ {
plugins[name] = append(plugins[name], pluginOption{
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(),
@ -90,37 +86,44 @@ func pluginsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request
}
}
return renderJSON(w, plugins)
return renderJSON(w, result)
}
func pluginsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
func settingsPutHandler(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")
}
var raw map[string]map[string]interface{}
// Parses the user and checks for error.
err := json.NewDecoder(r.Body).Decode(&raw)
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
View File

@ -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
}