Merge branch 'master' of github.com:hacdias/filemanager into triggers

This commit is contained in:
Maxime Daniel 2017-09-07 14:21:36 +02:00
commit d8548f00d5
70 changed files with 11068 additions and 1530 deletions

View File

@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="base" content="{{ .BaseURL }}"> <meta name="base" content="{{ .BaseURL }}">
<meta name="staticgen" content="{{ .StaticGen }}"> <meta name="staticgen" content="{{ .StaticGen }}">
<meta name="noauth" content="{{ .NoAuth }}">
<meta name="version" content="{{ .Version }}">
<title>File Manager</title> <title>File Manager</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
@ -22,7 +24,7 @@
<!-- Add to home screen for Windows --> <!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png"> <meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff"> <meta name="msapplication-TileColor" content="#2979ff">
<% for (var chunk of webpack.compilation.chunks) { <% for (var chunk of webpack.chunks) {
for (var file of chunk.files) { for (var file of chunk.files) {
if (file.match(/\.(js|css)$/)) { %> if (file.match(/\.(js|css)$/)) { %>
<link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %> <link rel="preload" href="{{ .BaseURL }}/<%= file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>

View File

@ -1,6 +1,7 @@
<template> <template>
<select v-on:change="change" :value="selected"> <select v-on:change="change" :value="selected">
<option value="en">{{ $t('languages.en') }}</option> <option value="en">{{ $t('languages.en') }}</option>
<option value="fr">{{ $t('languages.fr') }}</option>
<option value="pt">{{ $t('languages.pt') }}</option> <option value="pt">{{ $t('languages.pt') }}</option>
<option value="ja">{{ $t('languages.ja') }}</option> <option value="ja">{{ $t('languages.ja') }}</option>
<option value="zh-cn">{{ $t('languages.zhCN') }}</option> <option value="zh-cn">{{ $t('languages.zhCN') }}</option>

View File

@ -46,7 +46,7 @@
</button> </button>
</div> </div>
<div v-if="!$store.state.user.noAuth"> <div v-if="!$store.state.noAuth">
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')"> <router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
<i class="material-icons">settings_applications</i> <i class="material-icons">settings_applications</i>
<span>{{ $t('sidebar.settings') }}</span> <span>{{ $t('sidebar.settings') }}</span>
@ -59,7 +59,7 @@
</div> </div>
<p class="credits"> <p class="credits">
<span>{{ $t('sidebar.servedWith') }} <a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a>.</span> <span><a rel="noopener noreferrer" href="https://github.com/hacdias/filemanager">File Manager</a> v{{ version }}</span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span> <span><a @click="help">{{ $t('sidebar.help') }}</a></span>
</p> </p>
</nav> </nav>
@ -72,7 +72,7 @@ import auth from '@/utils/auth'
export default { export default {
name: 'sidebar', name: 'sidebar',
computed: { computed: {
...mapState(['user', 'staticGen']), ...mapState(['user', 'staticGen', 'version']),
active () { active () {
return this.$store.state.show === 'sidebar' return this.$store.state.show === 'sidebar'
} }

View File

@ -1,32 +1,35 @@
<template> <template>
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button"> <button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
<i class="material-icons">{{ icon() }}</i> <i class="material-icons">{{ icon }}</i>
<span>{{ $t('buttons.switchView') }}</span> <span>{{ $t('buttons.switchView') }}</span>
</button> </button>
</template> </template>
<script> <script>
import { mapState, mapMutations } from 'vuex'
import { updateUser } from '@/utils/api'
export default { export default {
name: 'switch-button', name: 'switch-button',
computed: {
...mapState(['user']),
icon: function () {
if (this.user.viewMode === 'mosaic') return 'view_list'
return 'view_module'
}
},
methods: { methods: {
...mapMutations(['updateUser']),
change: function (event) { change: function (event) {
// If we are on mobile we should close the dropdown. // If we are on mobile we should close the dropdown.
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
let display = 'mosaic' let user = {...this.user}
user.viewMode = (this.icon === 'view_list') ? 'list' : 'mosaic'
if (this.$store.state.req.display === 'mosaic') { updateUser(user, 'partial').then(() => {
display = 'list' this.updateUser({ viewMode: user.viewMode })
} }).catch(this.$showError)
this.$store.commit('listingDisplay', display)
let path = this.$store.state.baseURL
if (path === '') path = '/'
document.cookie = `display=${display}; max-age=31536000; path=${path}`
},
icon: function () {
if (this.$store.state.req.display === 'mosaic') return 'view_list'
return 'view_module'
} }
} }
} }

View File

@ -134,7 +134,7 @@ export default {
}) })
.catch(error => { .catch(error => {
buttons.done(button) buttons.done(button)
this.$store.commit('showError', error) this.$showError(error)
this.$store.commit('setSchedule', '') this.$store.commit('setSchedule', '')
}) })
} }

View File

@ -7,7 +7,7 @@
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple> <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
</div> </div>
<div v-else id="listing" <div v-else id="listing"
:class="req.display" :class="user.viewMode"
@dragenter="dragEnter" @dragenter="dragEnter"
@dragend="dragEnd"> @dragend="dragEnd">
<div> <div>
@ -98,7 +98,7 @@ export default {
name: 'listing', name: 'listing',
components: { Item }, components: { Item },
computed: { computed: {
...mapState(['req', 'selected']), ...mapState(['req', 'selected', 'user']),
nameSorted () { nameSorted () {
return (this.req.sort === 'name') return (this.req.sort === 'name')
}, },
@ -210,17 +210,13 @@ export default {
if (this.$store.state.clipboard.key === 'x') { if (this.$store.state.clipboard.key === 'x') {
api.move(items).then(() => { api.move(items).then(() => {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}).catch(error => { }).catch(this.$showError)
this.$store.commit('showError', error)
})
return return
} }
api.copy(items).then(() => { api.copy(items).then(() => {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}).catch(error => { }).catch(this.$showError)
this.$store.commit('showError', error)
})
}, },
resizeEvent () { resizeEvent () {
// Update the columns size based on the window width. // Update the columns size based on the window width.
@ -267,7 +263,7 @@ export default {
.then(req => { .then(req => {
this.checkConflict(files, req.items, base) this.checkConflict(files, req.items, base)
}) })
.catch(error => { console.log(error) }) .catch(this.$showError)
return return
} }
@ -348,7 +344,7 @@ export default {
}) })
.catch(error => { .catch(error => {
finish() finish()
this.$store.commit('showError', error) this.$showError(error)
}) })
return false return false

View File

@ -109,21 +109,24 @@ export default {
.then(() => { .then(() => {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}) })
.catch(error => { .catch(this.$showError)
this.$store.commit('showError', error)
})
}, },
click: function (event) { click: function (event) {
if (this.selectedCount !== 0) event.preventDefault() if (this.selectedCount !== 0) event.preventDefault()
if (this.$store.state.selected.indexOf(this.index) === -1) { if (this.$store.state.selected.indexOf(this.index) !== -1) {
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
} else {
this.removeSelected(this.index) this.removeSelected(this.index)
return
} }
return false if (event.shiftKey && this.selected.length === 1) {
let fi = (this.index > this.selected[0]) ? this.selected[0] : this.index
let la = (this.index > this.selected[0]) ? this.index : this.selected[0]
for (; fi <= la; fi++) this.addSelected(fi)
return
}
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
}, },
touchstart (event) { touchstart (event) {
setTimeout(() => { setTimeout(() => {

View File

@ -20,8 +20,8 @@
<div class="preview"> <div class="preview">
<img v-if="req.type == 'image'" :src="raw()"> <img v-if="req.type == 'image'" :src="raw()">
<audio v-else-if="req.type == 'audio'" :src="raw()" controls></audio> <audio v-else-if="req.type == 'audio'" :src="raw()" autoplay controls></audio>
<video v-else-if="req.type == 'video'" :src="raw()" controls> <video v-else-if="req.type == 'video'" :src="raw()" autoplay controls>
Sorry, your browser doesn't support embedded videos, Sorry, your browser doesn't support embedded videos,
but don't worry, you can <a :href="download()">download it</a> but don't worry, you can <a :href="download()">download it</a>
and watch it with your favorite video player! and watch it with your favorite video player!
@ -75,7 +75,7 @@ export default {
this.listing = req this.listing = req
this.updateLinks() this.updateLinks()
}) })
.catch(error => { console.log(error) }) .catch(this.$showError)
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keyup', this.key) window.removeEventListener('keyup', this.key)

View File

@ -56,7 +56,7 @@ export default {
}) })
.catch(error => { .catch(error => {
buttons.done('copy') buttons.done('copy')
this.$store.commit('showError', error) this.$showError(error)
}) })
} }
} }

View File

@ -43,7 +43,7 @@ export default {
}) })
.catch(error => { .catch(error => {
buttons.done('delete') buttons.done('delete')
this.$store.commit('showError', error) this.$showError(error)
}) })
return return
@ -70,7 +70,7 @@ export default {
.catch(error => { .catch(error => {
buttons.done('delete') buttons.done('delete')
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
this.$store.commit('showError', error) this.$showError(error)
}) })
} }
} }

View File

@ -1,31 +0,0 @@
<template>
<div class="prompt error">
<i class="material-icons">error_outline</i>
<h3>{{ $t('prompts.error') }}</h3>
<pre>{{ $store.state.showMessage }}</pre>
<div>
<button @click="close"
autofocus
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
<button @click="reportIssue"
class="cancel"
:aria-label="$t('buttons.reportIssue')"
:title="$t('buttons.reportIssue')">{{ $t('buttons.reportIssue') }}</button>
</div>
</div>
</template>
<script>
export default {
name: 'error',
methods: {
reportIssue () {
window.open('https://github.com/hacdias/filemanager/issues/new')
},
close () {
this.$store.commit('closeHovers')
}
}
}
</script>

View File

@ -53,7 +53,7 @@ export default {
// so we fetch the data from the previous directory. // so we fetch the data from the previous directory.
api.fetch(url.removeLastDir(this.$route.path)) api.fetch(url.removeLastDir(this.$route.path))
.then(this.fillOptions) .then(this.fillOptions)
.catch(this.showError) .catch(this.$showError)
}, },
methods: { methods: {
fillOptions (req) { fillOptions (req) {
@ -96,7 +96,7 @@ export default {
api.fetch(uri) api.fetch(uri)
.then(this.fillOptions) .then(this.fillOptions)
.catch(this.showError) .catch(this.$showError)
}, },
touchstart (event) { touchstart (event) {
let url = event.currentTarget.dataset.url let url = event.currentTarget.dataset.url

View File

@ -111,7 +111,7 @@ export default {
api.checksum(link, hash) api.checksum(link, hash)
.then((hash) => { event.target.innerHTML = hash }) .then((hash) => { event.target.innerHTML = hash })
.catch(error => { this.$store.commit('showError', error) }) .catch(this.$showError)
} }
} }
} }

View File

@ -56,7 +56,7 @@ export default {
}) })
.catch(error => { .catch(error => {
buttons.done('move') buttons.done('move')
this.$store.commit('showError', error) this.$showError(error)
}) })
event.preventDefault() event.preventDefault()

View File

@ -37,9 +37,7 @@ export default {
.then((url) => { .then((url) => {
this.$router.push({ path: url }) this.$router.push({ path: url })
}) })
.catch(error => { .catch(this.$showError)
this.$store.commit('showError', error)
})
}, },
new (url, type) { new (url, type) {
url = removePrefix(url) url = removePrefix(url)
@ -47,7 +45,7 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true) request.open('POST', `${this.$store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`) if (!this.$store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${this.$store.state.jwt}`)
request.setRequestHeader('Archetype', encodeURIComponent(type)) request.setRequestHeader('Archetype', encodeURIComponent(type))
request.onload = () => { request.onload = () => {

View File

@ -43,7 +43,7 @@ export default {
api.post(uri) api.post(uri)
.then(() => { this.$router.push({ path: uri }) }) .then(() => { this.$router.push({ path: uri }) })
.catch(error => { this.$store.commit('showError', error) }) .catch(this.$showError)
// Close the prompt // Close the prompt
this.$store.commit('closeHovers') this.$store.commit('closeHovers')

View File

@ -44,7 +44,7 @@ export default {
// Create the new file. // Create the new file.
api.post(uri) api.post(uri)
.then(() => { this.$router.push({ path: uri }) }) .then(() => { this.$router.push({ path: uri }) })
.catch(error => { this.$store.commit('showError', error) }) .catch(this.$showError)
// Close the prompt. // Close the prompt.
this.$store.commit('closeHovers') this.$store.commit('closeHovers')

View File

@ -9,8 +9,6 @@
<info v-else-if="showInfo"></info> <info v-else-if="showInfo"></info>
<move v-else-if="showMove"></move> <move v-else-if="showMove"></move>
<copy v-else-if="showCopy"></copy> <copy v-else-if="showCopy"></copy>
<error v-else-if="showError"></error>
<success v-else-if="showSuccess"></success>
<replace v-else-if="showReplace"></replace> <replace v-else-if="showReplace"></replace>
<schedule v-else-if="show === 'schedule'"></schedule> <schedule v-else-if="show === 'schedule'"></schedule>
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype> <new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
@ -27,8 +25,6 @@ import Rename from './Rename'
import Download from './Download' import Download from './Download'
import Move from './Move' import Move from './Move'
import Copy from './Copy' import Copy from './Copy'
import Error from './Error'
import Success from './Success'
import NewFile from './NewFile' import NewFile from './NewFile'
import NewDir from './NewDir' import NewDir from './NewDir'
import NewArchetype from './NewArchetype' import NewArchetype from './NewArchetype'
@ -47,9 +43,7 @@ export default {
NewArchetype, NewArchetype,
Schedule, Schedule,
Rename, Rename,
Error,
Download, Download,
Success,
Move, Move,
Copy, Copy,
Share, Share,
@ -70,8 +64,6 @@ export default {
}, },
computed: { computed: {
...mapState(['show', 'plugins']), ...mapState(['show', 'plugins']),
showError: function () { return this.show === 'error' },
showSuccess: function () { return this.show === 'success' },
showInfo: function () { return this.show === 'info' }, showInfo: function () { return this.show === 'info' },
showHelp: function () { return this.show === 'help' }, showHelp: function () { return this.show === 'help' },
showDelete: function () { return this.show === 'delete' }, showDelete: function () { return this.show === 'delete' },

View File

@ -68,7 +68,7 @@ export default {
} }
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}).catch(error => { }).catch(error => {
this.$store.commit('showError', error) this.$showError(error)
}) })
this.$store.commit('closeHovers') this.$store.commit('closeHovers')

View File

@ -18,7 +18,7 @@
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button> :title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
<button class="action copy" <button class="action copy-clipboard"
:data-clipboard-text="buildLink(link.hash)" :data-clipboard-text="buildLink(link.hash)"
:aria-label="$t('buttons.copyToClipboard')" :aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button> :title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
@ -54,7 +54,7 @@
</template> </template>
<script> <script>
import { mapState, mapMutations } from 'vuex' import { mapState } from 'vuex'
import { getShare, deleteShare, share } from '@/utils/api' import { getShare, deleteShare, share } from '@/utils/api'
import moment from 'moment' import moment from 'moment'
import Clipboard from 'clipboard' import Clipboard from 'clipboard'
@ -101,20 +101,25 @@ export default {
}) })
.catch(error => { .catch(error => {
if (error === 404) return if (error === 404) return
this.showError(error) this.$showError(error)
}) })
}, },
mounted () { mounted () {
this.clip = new Clipboard('.copy') this.clip = new Clipboard('.copy-clipboard')
this.clip.on('success', (e) => {
this.$showSuccess(this.$t('success.linkCopied'))
})
},
beforeDestroy () {
this.clip.destroy()
}, },
methods: { methods: {
...mapMutations([ 'showError' ]),
submit: function (event) { submit: function (event) {
if (!this.time) return if (!this.time) return
share(this.url, this.time, this.unit) share(this.url, this.time, this.unit)
.then(result => { this.links.push(result); this.sort() }) .then(result => { this.links.push(result); this.sort() })
.catch(error => { this.showError(error) }) .catch(this.$showError)
}, },
getPermalink (event) { getPermalink (event) {
share(this.url) share(this.url)
@ -123,7 +128,7 @@ export default {
this.sort() this.sort()
this.hasPermanent = true this.hasPermanent = true
}) })
.catch(error => { this.showError(error) }) .catch(this.$showError)
}, },
deleteLink (event, link) { deleteLink (event, link) {
event.preventDefault() event.preventDefault()
@ -132,7 +137,7 @@ export default {
if (!link.expires) this.hasPermanent = false if (!link.expires) this.hasPermanent = false
this.links = this.links.filter(item => item.hash !== link.hash) this.links = this.links.filter(item => item.hash !== link.hash)
}) })
.catch(error => { this.showError(error) }) .catch(this.$showError)
}, },
humanTime (time) { humanTime (time) {
return moment(time).fromNow() return moment(time).fromNow()

View File

@ -1,23 +0,0 @@
<template>
<div class="prompt success">
<i class="material-icons">done</i>
<h3>{{ $store.state.showMessage }}</h3>
<div>
<button @click="close"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')"
autofocus>{{ $t('buttons.ok') }}</button>
</div>
</div>
</template>
<script>
export default {
name: 'success',
methods: {
close () {
this.$store.commit('closeHovers')
}
}
}
</script>

View File

@ -2,7 +2,6 @@ body {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
padding-top: 4em; padding-top: 4em;
background-color: #f8f8f8; background-color: #f8f8f8;
user-select: none;
color: #212121; color: #212121;
} }

View File

@ -131,18 +131,21 @@ p code {
display: flex; display: flex;
color: rgb(84, 110, 122); color: rgb(84, 110, 122);
font-weight: 500; font-weight: 500;
padding: 0 0 1em;
margin: 0 0 1em; margin: 0 0 1em;
font-size: .8em; font-size: .8em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); text-align: center;
justify-content: space-between;
padding: 0;
} }
.dashboard #nav li { .dashboard #nav li {
width: 100%; width: 100%;
padding: 0 0 1em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
} }
.dashboard #nav li:last-child { .dashboard #nav li.active {
text-align: right border-color: #2196f3
} }
.dashboard #nav i { .dashboard #nav i {

View File

@ -206,3 +206,24 @@
margin-right: .5em; margin-right: .5em;
border: 1px solid #dadada; border: 1px solid #dadada;
} }
.prompt#share .action.copy-clipboard::after {
content: 'Copied!';
position: absolute;
left: -25%;
width: 150%;
font-size: .6em;
text-align: center;
background: #44a6f5;
color: #fff;
padding: .5em .2em;
border-radius: .4em;
top: -2em;
transition: .1s ease opacity;
opacity: 0;
}
.prompt#share .action.copy-clipboard.active::after {
opacity: 1;
}

View File

@ -1,4 +1,5 @@
@import "~normalize.css/normalize.css"; @import "~normalize.css/normalize.css";
@import "~noty/lib/noty.css";
@import "./fonts.css"; @import "./fonts.css";
@import "./base.css"; @import "./base.css";
@import "./header.css"; @import "./header.css";
@ -180,6 +181,17 @@
* PROMPT * * PROMPT *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
.noty_buttons {
text-align: right;
padding: 0 10px 10px !important;
}
.noty_buttons button {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0,0,0,0.1);
box-shadow: 0 0 0 0;
font-size: 14px;
}
/* * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * *
* FOOTER * * FOOTER *

View File

@ -26,11 +26,13 @@ buttons:
publish: Publish publish: Publish
selectMultiple: Select multiple selectMultiple: Select multiple
schedule: Schedule schedule: Schedule
switchView: Swicth view switchView: Switch view
toggleSidebar: Toggle sidebar toggleSidebar: Toggle sidebar
update: Update update: Update
upload: Upload upload: Upload
permalink: Get Permanent Link permalink: Get Permanent Link
success:
linkCopied: Link copied!
errors: errors:
forbidden: You're not welcome here. forbidden: You're not welcome here.
internal: Something really went wrong. internal: Something really went wrong.
@ -122,6 +124,7 @@ settings:
examples: Examples examples: Examples
globalSettings: Global Settings globalSettings: Global Settings
language: Language language: Language
lockPassword: Prevent the user from changing the password
newPassword: Your new password newPassword: Your new password
newPasswordConfirm: Confirm your new password newPasswordConfirm: Confirm your new password
newUser: New User newUser: New User
@ -165,7 +168,6 @@ sidebar:
myFiles: My files myFiles: My files
newFile: New file newFile: New file
newFolder: New folder newFolder: New folder
servedWith: Served with
settings: Settings settings: Settings
siteSettings: Site Settings siteSettings: Site Settings
hugoNew: Hugo New hugoNew: Hugo New
@ -185,6 +187,7 @@ search:
writeToSearch: Write here to search writeToSearch: Write here to search
languages: languages:
en: English en: English
fr: Français
pt: Português pt: Português
ja: 日本語 ja: 日本語
zhCN: 中文 (简体) zhCN: 中文 (简体)

193
assets/src/i18n/fr.yaml Normal file
View File

@ -0,0 +1,193 @@
permanent: Permanent
buttons:
cancel: Annuler
close: Fermer
copy: Copier
copyFile: Copier le fichier
copyToClipboard: Copier dans le presse-papier
create: Créer
delete: Supprimer
download: Télécharger
info: Info
more: Plus
move: Déplacer
moveFile: Déplacer le fichier
new: Nouveau
next: Suivant
ok: OK
replace: Remplacer
previous: Précédent
rename: Renommer
reportIssue: Rapport d'erreur
save: Enregistrer
search: Chercher
select: Sélectionner
share: Partager
publish: Publier
selectMultiple: Sélection multiple
schedule: Fixer la date
switchView: Changer le mode d'affichage
toggleSidebar: Afficher/Masquer la barre latérale
update: Mettre à jour
upload: Importer
permalink: Obtenir un lien permanent
errors:
forbidden: Vous n'êtes pas autorisé à être ici.
internal: Aïe ! Quelque chose s'est mal passé.
notFound: Impossible d'accéder à cet emplacement.
files:
folders: Dossiers
files: Fichiers
body: Corps
clear: Fermer
closePreview: Fermer la prévisualisation
home: Accueil
lastModified: Dernière modification
loading: Chargement...
lonely: Il semble qu'il n'y ai rien par ici...
metadata: Metadonnées
multipleSelectionEnabled: Sélection multiple activée
name: Nom
size: Taille
sortByName: Trier par nom
sortBySize: Trier par taille
sortByLastModified: Trier par date de dernière modification
help:
click: Sélectionner un élément
ctrl:
click: Sélectionner plusieurs éléments
f: Ouvrir l'invité de recherche
s: Télécharger l'élément actuel
del: Supprimer les éléments sélectionnés
doubleClick: Ouvrir un élément
esc: Désélectionner et/ou fermer la boîte de dialogue
f1: Ouvrir l'aide
f2: Renommer le fichier
help: Aide
login:
password: Mot de passe
submit: Se connecter
username: Utilisateur
wrongCredentials: Identifiants incorrects !
prompts:
copy: Copier
copyMessage: 'Choisissez l''emplacement où copier la sélection :'
currentlyNavigating: 'Dossier courant :'
deleteMessageMultiple: Etes-vous sûr de vouloir supprimer ces {count} élément(s) ?
deleteMessageSingle: Etes-vous sûr de vouloir supprimer cet élément ?
deleteTitle: Supprimer
displayName: 'Nom :'
download: Télécharger
downloadMessage: 'Choisissez le format de téléchargement :'
error: Quelque chose s'est mal passé
fileInfo: Informations
filesSelected: "{count} éléments sélectionnés"
lastModified: Dernière modification
move: Déplacer
moveMessage: 'Choisissez l''emplacement où déplacer la sélection :'
newDir: Nouveau dossier
newDirMessage: 'Nom du nouveau dossier :'
newFile: Nouveau fichier
newFileMessage: 'Nom du nouveau fichier :'
numberDirs: Nombre de dossiers
numberFiles: Nombre de fichiers
replace: Remplacer
replaceMessage: >
Un des fichiers que vous êtes en train d'importer a le même nom qu'un autre déjà présent.
Voulez-vous remplacer le fichier actuel par le nouveau ?
rename: Renommer
renameMessage: Nouveau nom pour
show: Montrer
size: Taille
schedule: Fixer la date
scheduleMessage: Choisissez une date pour planifier la publication de ce post
newArchetype: Créer un nouveau post basé sur un archétype. Votre fichier sera créé dans le dossier de contenu.
settings:
admin: Admin
administrator: Administrateur
allowCommands: Exécuter des commandes
allowEdit: Editer, renommer et supprimer des fichiers ou des dossiers
allowNew: Créer de nouveaux fichiers et dossiers
allowPublish: Publier de nouveaux posts et pages
avoidChanges: "(Laisser vide pour conserver l'actuel)"
changePassword: Modifier le mot de passe
commands: Commandes
commandsHelp: >
Ici vous pouvez définir des commandes qui seront exécutées lors de l'évènement correspondant.
Vous devez indiquer une commande par ligne. Si l'évènement est en rapport avec des fichiers,
par exemple avant et après enregistrement, la variable d'environement "file" sera disponible
et contiendra le chemin d'accès vers le fichier.
commandsUpdated: Commandes mises à jour !
customStylesheet: Feuille de style personnalisée
examples: Exemples
globalSettings: Paramètres généraux
language: Langue
newPassword: Votre nouveau mot de passe
newPasswordConfirm: Confirmation du nouveau mot de passe
newUser: Nouvel Utilisateur
password: Mot de passe
passwordUpdated: Mot de passe mis à jour !
permissions: Permissions
permissionsHelp: >
Vous pouvez définir l'utilisateur comme étant un administrateur ou encore choisir les
permissions individuellement. Si vous sélectionnez "Administrateur", toutes les autres
options seront automatiquement activées. La gestion des utilisateurs est un privilège que
seul l'administrateur possède.
profileSettings: Paramètres du profil
ruleExample1: Bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers
ruleExample2: Bloque l'accès au fichier nommé "Caddyfile" à la racine du dossier utilisateur
rules: Règles
rulesHelp1: >
Vous pouvez définir ici un ensemble de règles pour cet utilisateur.
Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur.
Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur.
rulesHelp2: >
Chaque règle est définie sur une ligne différente et doit commencer par le mot clé {0} ou {1}.
Vous devez ensuite ajouter {2} si vous utilisez une expression régulière puis l'expression en question ou bien seulement le chemin d'accès.
scope: Portée du dossier utilisateur
settingsUpdated: Les paramètres ont été mis à jour !
user: Utilisateur
userCommands: Commandes
userCommandsHelp: 'Une liste séparée par des espaces des commandes permises pour l''utilisateur. Exemple :'
userCreated: Utilisateur créé !
userDeleted: Utilisateur supprimé !
userManagement: Gestion des utilisateurs
username: Nom d'utilisateur
users: Utilisateurs
userUpdated: Utilisateur mis à jour !
sidebar:
help: Aide
logout: Se déconnecter
myFiles: Mes fichiers
newFile: Nouveau fichier
newFolder: Nouveau dossier
settings: Paramètres
siteSettings: Paramètres du site
hugoNew: Nouveau Hugo
preview: Prévisualiser
search:
images: Images
music: Musique
pdf: PDF
pressToExecute: Appuyez sur Entrée pour exécuter
pressToSearch: Appuyez sur Entrée pour lancer la recherche
search: Recherche en cours...
searchOrCommand: Rechercher ou exécuter une commande...
searchOrSupportedCommand: 'Lancez une recherche ou exécutez une commande parmis les suivantes :'
type: Tapez votre recherche et appuyez sur Entrée
types: Types
video: Video
writeToSearch: Ecrivez ici pour lancer une recherche
languages:
en: English
fr: Français
pt: Português
ja: 日本語
zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time:
unit: Unité de temps
seconds: Secondes
minutes: Minutes
hours: Heures
days: Jours

View File

@ -1,6 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
import en from './en.yaml' import en from './en.yaml'
import fr from './fr.yaml'
import pt from './pt.yaml' import pt from './pt.yaml'
import ja from './ja.yaml' import ja from './ja.yaml'
import zhCN from './zh-cn.yaml' import zhCN from './zh-cn.yaml'
@ -13,6 +14,7 @@ const i18n = new VueI18n({
fallbackLocale: 'en', fallbackLocale: 'en',
messages: { messages: {
'en': en, 'en': en,
'fr': fr,
'pt': pt, 'pt': pt,
'ja': ja, 'ja': ja,
'zh-cn': zhCN, 'zh-cn': zhCN,

View File

@ -31,6 +31,8 @@ buttons:
update: 更新 update: 更新
upload: アップロード upload: アップロード
permalink: 固定リンク permalink: 固定リンク
success:
linkCopied: リンクがコピーされました!
errors: errors:
forbidden: アクセスが拒否されました。 forbidden: アクセスが拒否されました。
internal: 内部エラーが発生しました。 internal: 内部エラーが発生しました。
@ -122,6 +124,7 @@ settings:
examples: examples:
globalSettings: グローバル設定 globalSettings: グローバル設定
language: 言語 language: 言語
lockPassowrd: 新しいパスワードを変更に禁止
newPassword: 新しいパスワード newPassword: 新しいパスワード
newPasswordConfirm: 新しいパスワードを確認します newPasswordConfirm: 新しいパスワードを確認します
newUser: 新しいユーザー newUser: 新しいユーザー
@ -165,7 +168,6 @@ sidebar:
myFiles: 私のファイル myFiles: 私のファイル
newFile: 新しいファイルを作成 newFile: 新しいファイルを作成
newFolder: 新しいフォルダを作成 newFolder: 新しいフォルダを作成
servedWith: サービス提供者
settings: 設定 settings: 設定
siteSettings: サイト設定 siteSettings: サイト設定
hugoNew: Hugo New hugoNew: Hugo New
@ -185,6 +187,7 @@ search:
writeToSearch: ここにキーワードを入力してください writeToSearch: ここにキーワードを入力してください
languages: languages:
en: English en: English
fr: Français
pt: Português pt: Português
ja: 日本語 ja: 日本語
zhCN: 中文 (简体) zhCN: 中文 (简体)

View File

@ -31,6 +31,8 @@ buttons:
update: Atualizar update: Atualizar
upload: Enviar upload: Enviar
permalink: Obter link permanente permalink: Obter link permanente
success:
linkCopied: Link copiado!
errors: errors:
forbidden: Tu não és bem-vindo aqui. forbidden: Tu não és bem-vindo aqui.
internal: Algo correu bastante mal. internal: Algo correu bastante mal.
@ -66,6 +68,7 @@ help:
help: Ajuda help: Ajuda
languages: languages:
en: English en: English
fr: Français
pt: Português pt: Português
ja: 日本語 ja: 日本語
zhCN: 中文 (简体) zhCN: 中文 (简体)
@ -142,6 +145,7 @@ settings:
examples: Exemplos examples: Exemplos
globalSettings: Configurações Globais globalSettings: Configurações Globais
language: Linguagem language: Linguagem
lockPassword: Não permitir que o utilizador altere a palavra-passe
newPassword: Nova palavra-passe newPassword: Nova palavra-passe
newPasswordConfirm: Confirme a nova palavra-passe newPasswordConfirm: Confirme a nova palavra-passe
newUser: Novo Utilizador newUser: Novo Utilizador
@ -189,7 +193,6 @@ sidebar:
newFile: Novo ficheiro newFile: Novo ficheiro
newFolder: Nova pasta newFolder: Nova pasta
preview: Pré-visualizar preview: Pré-visualizar
servedWith: Servido com
settings: Configurações settings: Configurações
siteSettings: Configurações do Site siteSettings: Configurações do Site
time: time:

View File

@ -31,6 +31,8 @@ buttons:
update: 更新 update: 更新
upload: 上传 upload: 上传
permalink: 获取永久链接 permalink: 获取永久链接
success:
linkCopied: 链接已复制!
errors: errors:
forbidden: 你被禁止访问。 forbidden: 你被禁止访问。
internal: 内部出现麻烦了。 internal: 内部出现麻烦了。
@ -121,6 +123,7 @@ settings:
examples: 例子 examples: 例子
globalSettings: 全局设置 globalSettings: 全局设置
language: 语言 language: 语言
lockPassowrd: 禁止用户修改密码
newPassword: 您的新密码 newPassword: 您的新密码
newPasswordConfirm: 重输一遍新密码 newPasswordConfirm: 重输一遍新密码
newUser: 新建用户 newUser: 新建用户
@ -163,7 +166,6 @@ sidebar:
myFiles: 我的文件 myFiles: 我的文件
newFile: 新建文件 newFile: 新建文件
newFolder: 新建文件夹 newFolder: 新建文件夹
servedWith: '服务提供者:'
settings: 设置 settings: 设置
siteSettings: 网站设置 siteSettings: 网站设置
hugoNew: Hugo New hugoNew: Hugo New
@ -183,6 +185,7 @@ search:
writeToSearch: 请输入要搜索的内容 writeToSearch: 请输入要搜索的内容
languages: languages:
en: English en: English
fr: Français
pt: Português pt: Português
ja: 日本語 ja: 日本語
zhCN: 中文 (简体) zhCN: 中文 (简体)

View File

@ -4,7 +4,7 @@ buttons:
close: 關閉 close: 關閉
copy: 複製 copy: 複製
copyFile: 複製檔案 copyFile: 複製檔案
copyToClipboard: 複製到剪貼 copyToClipboard: 複製到剪貼簿
create: 建立 create: 建立
delete: 刪除 delete: 刪除
download: 下載 download: 下載
@ -31,8 +31,10 @@ buttons:
update: 更新 update: 更新
upload: 上傳 upload: 上傳
permalink: 獲取永久連結 permalink: 獲取永久連結
success:
linkCopied: 連結已複製!
errors: errors:
forbidden: 你被禁止訪問。 forbidden: 你被禁止存取
internal: 內部出現麻煩了。 internal: 內部出現麻煩了。
notFound: 找不到檔案。 notFound: 找不到檔案。
files: files:
@ -45,7 +47,7 @@ files:
lastModified: 最後修改 lastModified: 最後修改
loading: 讀取中... loading: 讀取中...
lonely: 這裡沒有任何檔案... lonely: 這裡沒有任何檔案...
metadata: 中繼資料 metadata: 詮釋資料
multipleSelectionEnabled: 多選模式已開啟 multipleSelectionEnabled: 多選模式已開啟
name: 名稱 name: 名稱
size: 大小 size: 大小
@ -100,15 +102,15 @@ prompts:
show: 顯示 show: 顯示
size: 大小 size: 大小
schedule: 計畫 schedule: 計畫
scheduleMessage: 請選擇發佈這篇帖子的日期。 scheduleMessage: 請選擇發佈這篇貼文的日期。
newArchetype: 建立一個基於原型的新帖子。您的檔案將會建立在內容資料夾中。 newArchetype: 建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。
settings: settings:
admin: 管理員 admin: 管理員
administrator: 管理員 administrator: 管理員
allowCommands: 執行命令 allowCommands: 執行命令
allowEdit: 編輯、重命名或刪除檔案/目錄 allowEdit: 編輯、重命名或刪除檔案/目錄
allowNew: 創建新檔案和目錄 allowNew: 創建新檔案和目錄
allowPublish: 發佈新的帖子與頁面 allowPublish: 發佈新的貼文與頁面
avoidChanges: '(留空以避免更改)' avoidChanges: '(留空以避免更改)'
changePassword: 更改密碼 changePassword: 更改密碼
commands: 命令 commands: 命令
@ -118,52 +120,52 @@ settings:
則檔案的路徑會被賦值給環境變數 \"file\"。" 則檔案的路徑會被賦值給環境變數 \"file\"。"
commandsUpdated: 命令已更新! commandsUpdated: 命令已更新!
customStylesheet: 自定義樣式表 customStylesheet: 自定義樣式表
examples: examples:
globalSettings: 全域設定 globalSettings: 全域設定
language: 語言 language: 語言
lockPassword: 禁止使用者修改密碼
newPassword: 您的新密碼 newPassword: 您的新密碼
newPasswordConfirm: 重輸一遍新密碼 newPasswordConfirm: 重輸一遍新密碼
newUser: 建立用戶 newUser: 建立使用者
password: 密碼 password: 密碼
passwordUpdated: 密碼已更新! passwordUpdated: 密碼已更新!
permissions: 權限 permissions: 權限
permissionsHelp: "\ permissionsHelp: "\
您可以將該用戶設置為管理員,也可以單獨選擇各項權限。\ 您可以將該使用者設置為管理員,也可以單獨選擇各項權限。\
如果選擇了“管理員”,則其他的選項會被自動勾上,\ 如果選擇了“管理員”,則其他的選項會被自動勾上,\
同時該用戶可以管理其他用戶。" 同時該使用者可以管理其他使用者。"
profileSettings: 設定檔設定 profileSettings: 設定檔設定
ruleExample1: "\ ruleExample1: "\
封鎖用戶訪問所有資料夾下任何以 . 開頭的檔案\ 封鎖使用者存取所有資料夾下任何以 . 開頭的檔案\
(隱藏文件, 例如: .git, .gitignore)。" (隱藏文件, 例如: .git, .gitignore)。"
ruleExample2: 封鎖用戶訪問其目錄範圍的根目錄下名為 Caddyfile 的檔案。 ruleExample2: 封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。
rules: 規則 rules: 規則
rulesHelp1: "\ rulesHelp1: "\
您可以為該用戶製定一組黑名單或白名單式的規則,\ 您可以為該使用者製定一組黑名單或白名單式的規則,\
被屏蔽的檔案將不會顯示在清單中,用戶也無權限訪問,\ 被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,\
支持相對於目錄範圍的路徑。" 支持相對於目錄範圍的路徑。"
rulesHelp2: "\ rulesHelp2: "\
每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\ 每行一條規則,且必須以關鍵字 {0} 或 {1} 開頭。\
如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。" 如要使用規則運算式,請在加上 {2} 之後再附上運算式或路徑。"
scope: 目錄範圍 scope: 目錄範圍
settingsUpdated: 設定已更新! settingsUpdated: 設定已更新!
user: 用戶 user: 使用者
userCommands: 用戶命令 userCommands: 使用者命令
userCommandsHelp: "\ userCommandsHelp: "\
指定該用戶可以執行的命令,用空格分隔。\ 指定該使用者可以執行的命令,用空格分隔。\
例如:" 例如:"
userCreated: 用戶已建立! userCreated: 使用者已建立!
userDeleted: 用戶已刪除! userDeleted: 使用者已刪除!
userManagement: 用戶管理 userManagement: 使用者管理
username: 用戶名 username: 使用者名稱
users: 用戶 users: 使用者
userUpdated: 用戶已更新! userUpdated: 使用者已更新!
sidebar: sidebar:
help: 幫助 help: 幫助
logout: 登出 logout: 登出
myFiles: 我的檔案 myFiles: 我的檔案
newFile: 建立檔案 newFile: 建立檔案
newFolder: 建立資料夾 newFolder: 建立資料夾
servedWith: '服務提供者:'
settings: 設定 settings: 設定
siteSettings: 網站設定 siteSettings: 網站設定
hugoNew: Hugo New hugoNew: Hugo New
@ -183,9 +185,11 @@ search:
writeToSearch: 請輸入要搜尋的內容 writeToSearch: 請輸入要搜尋的內容
languages: languages:
en: English en: English
fr: Français
pt: Português pt: Português
ja: 日本語 ja: 日本語
zhCN: 中文 (简体) zhCN: 中文 (简体)
zhTW: 中文 (繁體)
time: time:
unit: 時間單位 unit: 時間單位
seconds: seconds:

View File

@ -3,9 +3,47 @@ import App from './App'
import store from './store' import store from './store'
import router from './router' import router from './router'
import i18n from './i18n' import i18n from './i18n'
import Noty from 'noty'
Vue.config.productionTip = true Vue.config.productionTip = true
const notyDefault = {
type: 'info',
layout: 'bottomRight',
timeout: 1000,
progressBar: true
}
Vue.prototype.$noty = function (opts) {
new Noty(Object.assign({}, notyDefault, opts)).show()
}
Vue.prototype.$showSuccess = function (message) {
new Noty(Object.assign({}, notyDefault, {
text: message,
type: 'success'
})).show()
}
Vue.prototype.$showError = function (error) {
// TODO: add btns: close and report issue
let n = new Noty(Object.assign({}, notyDefault, {
text: error,
type: 'error',
timeout: null,
buttons: [
Noty.button(i18n.t('buttons.reportIssue'), 'cancel', function () {
window.open('https://github.com/hacdias/filemanager/issues/new')
}),
Noty.button(i18n.t('buttons.close'), '', function () {
n.close()
})
]
}))
n.show()
}
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({
el: '#app', el: '#app',

View File

@ -14,6 +14,8 @@ const state = {
}, },
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'), staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'), baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'),
version: document.querySelector('meta[name="version"]').getAttribute('content'),
jwt: '', jwt: '',
progress: 0, progress: 0,
schedule: '', schedule: '',

View File

@ -45,8 +45,12 @@ const mutations = {
resetSelected: (state) => { resetSelected: (state) => {
state.selected = [] state.selected = []
}, },
listingDisplay: (state, value) => { updateUser: (state, value) => {
state.req.display = value if (typeof value !== 'object') return
for (let field in value) {
state.user[field] = value[field]
}
}, },
updateRequest: (state, value) => { updateRequest: (state, value) => {
state.req = value state.req = value

View File

@ -18,7 +18,7 @@ export function fetch (url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/resource${url}`, true) request.open('GET', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
switch (request.status) { switch (request.status) {
@ -41,7 +41,7 @@ export function remove (url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/resource${url}`, true) request.open('DELETE', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {
@ -62,7 +62,7 @@ export function post (url, content = '', overwrite = false, onupload) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/resource${url}`, true) request.open('POST', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
if (typeof onupload === 'function') { if (typeof onupload === 'function') {
request.upload.onprogress = onupload request.upload.onprogress = onupload
@ -95,7 +95,7 @@ export function put (url, content = '', publish = false, date = '') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true) request.open('PUT', `${store.state.baseURL}/api/resource${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.setRequestHeader('Publish', publish) request.setRequestHeader('Publish', publish)
if (date !== '') { if (date !== '') {
@ -125,7 +125,7 @@ function moveCopy (items, copy = false) {
promises.push(new Promise((resolve, reject) => { promises.push(new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('PATCH', `${store.state.baseURL}/api/resource${from}`, true) request.open('PATCH', `${store.state.baseURL}/api/resource${from}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.setRequestHeader('Destination', to) request.setRequestHeader('Destination', to)
if (copy) { if (copy) {
@ -162,7 +162,7 @@ export function checksum (url, algo) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/checksum${url}?algo=${algo}`, true) request.open('GET', `${store.state.baseURL}/api/checksum${url}?algo=${algo}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {
@ -226,7 +226,7 @@ export function getSettings () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/settings/`, true) request.open('GET', `${store.state.baseURL}/api/settings/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
switch (request.status) { switch (request.status) {
@ -255,7 +255,7 @@ export function updateSettings (param, which) {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/settings/`, true) request.open('PUT', `${store.state.baseURL}/api/settings/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
switch (request.status) { switch (request.status) {
@ -278,7 +278,7 @@ export function getUsers () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/`, true) request.open('GET', `${store.state.baseURL}/api/users/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
switch (request.status) { switch (request.status) {
@ -299,7 +299,7 @@ export function getUser (id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/users/${id}`, true) request.open('GET', `${store.state.baseURL}/api/users/${id}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
switch (request.status) { switch (request.status) {
@ -320,7 +320,7 @@ export function newUser (user) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/users/`, true) request.open('POST', `${store.state.baseURL}/api/users/`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
switch (request.status) { switch (request.status) {
@ -345,7 +345,7 @@ export function updateUser (user, which) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true) request.open('PUT', `${store.state.baseURL}/api/users/${user.ID}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
switch (request.status) { switch (request.status) {
@ -370,7 +370,7 @@ export function deleteUser (id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true) request.open('DELETE', `${store.state.baseURL}/api/users/${id}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
switch (request.status) { switch (request.status) {
@ -395,7 +395,7 @@ export function getShare (url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/share${url}`, true) request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {
@ -414,7 +414,7 @@ export function deleteShare (hash) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true) request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {
@ -439,7 +439,7 @@ export function share (url, expires = '', unit = 'hours') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('POST', url, true) request.open('POST', url, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {

View File

@ -16,7 +16,7 @@ function loggedIn () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest() let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/auth/renew`, true) request.open('GET', `${store.state.baseURL}/api/auth/renew`, true)
request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`) if (!store.state.noAuth) request.setRequestHeader('Authorization', `Bearer ${cookie('auth')}`)
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {

View File

@ -1,16 +1,9 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<ul id="nav"> <ul id="nav">
<li> <li><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
<router-link to="/settings/profile"> <li class="active"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.profileSettings') }} <li><router-link to="/users">{{ $t('settings.userManagement') }}</router-link></li>
</router-link>
</li>
<li>
<router-link to="/users">
{{ $t('settings.userManagement') }} <i class="material-icons">keyboard_arrow_right</i>
</router-link>
</li>
</ul> </ul>
<h1>{{ $t('settings.globalSettings') }}</h1> <h1>{{ $t('settings.globalSettings') }}</h1>
@ -45,7 +38,7 @@
</template> </template>
<script> <script>
import { mapState, mapMutations } from 'vuex' import { mapState } from 'vuex'
import { getSettings, updateSettings } from '@/utils/api' import { getSettings, updateSettings } from '@/utils/api'
export default { export default {
@ -73,10 +66,9 @@ export default {
}) })
} }
}) })
.catch(error => { this.showError(error) }) .catch(error => { this.$showError(error) })
}, },
methods: { methods: {
...mapMutations([ 'showSuccess', 'showError' ]),
capitalize (name, where = '_') { capitalize (name, where = '_') {
if (where === 'caps') where = /(?=[A-Z])/ if (where === 'caps') where = /(?=[A-Z])/
let splitted = name.split(where) let splitted = name.split(where)
@ -103,8 +95,8 @@ export default {
} }
updateSettings(commands, 'commands') updateSettings(commands, 'commands')
.then(() => { this.showSuccess(this.$t('settings.commandsUpdated')) }) .then(() => { this.$showSuccess(this.$t('settings.commandsUpdated')) })
.catch(error => { this.showError(error) }) .catch(error => { this.$showError(error) })
}, },
saveStaticGen (event) { saveStaticGen (event) {
event.preventDefault() event.preventDefault()
@ -124,8 +116,8 @@ export default {
} }
updateSettings(staticGen, 'staticGen') updateSettings(staticGen, 'staticGen')
.then(() => { this.showSuccess(this.$t('settings.settingsUpdated')) }) .then(() => { this.$showSuccess(this.$t('settings.settingsUpdated')) })
.catch(error => { this.showError(error) }) .catch(error => { this.$showError(error) })
}, },
parseStaticGen (staticgen) { parseStaticGen (staticgen) {
for (let option of staticgen) { for (let option of staticgen) {

View File

@ -1,11 +1,9 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<ul id="nav" v-if="user.admin"> <ul id="nav" v-if="user.admin">
<li> <li class="active"><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
<router-link to="/settings/global"> <li><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
{{ $t('settings.globalSettings') }} <i class="material-icons">keyboard_arrow_right</i> <li><router-link to="/users">{{ $t('settings.userManagement') }}</router-link></li>
</router-link>
</li>
</ul> </ul>
<h1>{{ $t('settings.profileSettings') }}</h1> <h1>{{ $t('settings.profileSettings') }}</h1>
@ -18,7 +16,7 @@
<p><input type="submit" :value="$t('buttons.update')"></p> <p><input type="submit" :value="$t('buttons.update')"></p>
</form> </form>
<form @submit="updatePassword"> <form v-if="!user.lockPassword" @submit="updatePassword">
<h3>{{ $t('settings.changePassword') }}</h3> <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.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 :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password"></p>
@ -28,7 +26,7 @@
</template> </template>
<script> <script>
import { mapState, mapMutations } from 'vuex' import { mapState } from 'vuex'
import { updateUser } from '@/utils/api' import { updateUser } from '@/utils/api'
import Languages from '@/components/Languages' import Languages from '@/components/Languages'
@ -64,7 +62,6 @@ export default {
this.locale = this.user.locale this.locale = this.user.locale
}, },
methods: { methods: {
...mapMutations([ 'showSuccess' ]),
updatePassword (event) { updatePassword (event) {
event.preventDefault() event.preventDefault()
@ -78,9 +75,9 @@ export default {
} }
updateUser(user, 'password').then(location => { updateUser(user, 'password').then(location => {
this.showSuccess(this.$t('settings.passwordUpdated')) this.$showSuccess(this.$t('settings.passwordUpdated'))
}).catch(e => { }).catch(e => {
this.$store.commit('showError', e) this.$showError(e)
}) })
}, },
updateSettings (event) { updateSettings (event) {
@ -93,9 +90,9 @@ export default {
updateUser(user, 'partial').then(location => { updateUser(user, 'partial').then(location => {
this.$store.commit('setUser', user) this.$store.commit('setUser', user)
this.$emit('css-updated') this.$emit('css-updated')
this.showSuccess(this.$t('settings.settingsUpdated')) this.$showSuccess(this.$t('settings.settingsUpdated'))
}).catch(e => { }).catch(e => {
this.$store.commit('showError', e) this.$showError(e)
}) })
} }
} }

View File

@ -2,12 +2,9 @@
<div> <div>
<form @submit="save" class="dashboard"> <form @submit="save" class="dashboard">
<ul id="nav"> <ul id="nav">
<li> <li><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
<router-link to="/users"> <li><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.userManagement') }} <li><router-link to="/users">{{ $t('settings.userManagement') }}</router-link></li>
</router-link>
</li>
<li></li>
</ul> </ul>
<h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1> <h1 v-if="id === 0">{{ $t('settings.newUser') }}</h1>
@ -21,6 +18,8 @@
<languages id="locale" :selected.sync="locale"></languages> <languages id="locale" :selected.sync="locale"></languages>
</p> </p>
<p><input type="checkbox" :disabled="admin" v-model="lockPassword"> {{ $t('settings.lockPassword') }}</p>
<h2>{{ $t('settings.permissions') }}</h2> <h2>{{ $t('settings.permissions') }}</h2>
<p class="small">{{ $t('settings.permissionsHelp') }}</p> <p class="small">{{ $t('settings.permissionsHelp') }}</p>
@ -93,6 +92,7 @@ export default {
allowEdit: false, allowEdit: false,
allowCommands: false, allowCommands: false,
allowPublish: false, allowPublish: false,
lockPassword: false,
permissions: {}, permissions: {},
password: '', password: '',
username: '', username: '',
@ -120,6 +120,7 @@ export default {
this.allowEdit = true this.allowEdit = true
this.allowNew = true this.allowNew = true
this.allowPublish = true this.allowPublish = true
this.lockPassword = false
for (let key in this.permissions) { for (let key in this.permissions) {
this.permissions[key] = true this.permissions[key] = true
} }
@ -141,6 +142,7 @@ export default {
this.allowNew = user.allowNew this.allowNew = user.allowNew
this.allowEdit = user.allowEdit this.allowEdit = user.allowEdit
this.allowPublish = user.allowPublish this.allowPublish = user.allowPublish
this.lockPassword = user.lockPassword
this.filesystem = user.filesystem this.filesystem = user.filesystem
this.username = user.username this.username = user.username
this.commands = user.commands.join(' ') this.commands = user.commands.join(' ')
@ -187,6 +189,7 @@ export default {
this.allowPublish = false this.allowPublish = false
this.permissins = {} this.permissins = {}
this.allowCommands = false this.allowCommands = false
this.lockPassword = false
this.password = '' this.password = ''
this.username = '' this.username = ''
this.filesystem = '' this.filesystem = ''
@ -203,9 +206,9 @@ export default {
deleteUser(this.id).then(location => { deleteUser(this.id).then(location => {
this.$router.push({ path: '/users' }) this.$router.push({ path: '/users' })
this.$store.commit('showSuccess', this.$t('settings.userDeleted')) this.$showSuccess(this.$t('settings.userDeleted'))
}).catch(e => { }).catch(e => {
this.$store.commit('showError', e) this.$showError(e)
}) })
}, },
save (event) { save (event) {
@ -215,9 +218,9 @@ export default {
if (this.$route.path === '/users/new') { if (this.$route.path === '/users/new') {
newUser(user).then(location => { newUser(user).then(location => {
this.$router.push({ path: location }) this.$router.push({ path: location })
this.$store.commit('showSuccess', this.$t('settings.userCreated')) this.$showSuccess(this.$t('settings.userCreated'))
}).catch(e => { }).catch(e => {
this.$store.commit('showError', e) this.$showError(e)
}) })
return return
@ -228,9 +231,9 @@ export default {
this.$store.commit('setUser', user) this.$store.commit('setUser', user)
} }
this.$store.commit('showSuccess', this.$t('settings.userUpdated')) this.$showSuccess(this.$t('settings.userUpdated'))
}).catch(e => { }).catch(e => {
this.$store.commit('showError', e) this.$showError(e)
}) })
}, },
parseForm () { parseForm () {
@ -238,6 +241,7 @@ export default {
ID: this.id, ID: this.id,
username: this.username, username: this.username,
password: this.password, password: this.password,
lockPassword: this.lockPassword,
filesystem: this.filesystem, filesystem: this.filesystem,
admin: this.admin, admin: this.admin,
allowCommands: this.allowCommands, allowCommands: this.allowCommands,

View File

@ -1,12 +1,9 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<ul id="nav"> <ul id="nav">
<li> <li><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
<router-link to="/settings/global"> <li><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
<i class="material-icons">keyboard_arrow_left</i> {{ $t('settings.globalSettings') }} <li class="active"><router-link to="/users">{{ $t('settings.userManagement') }}</router-link></li>
</router-link>
</li>
<li></li>
</ul> </ul>
<h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1> <h1>{{ $t('settings.users') }} <router-link to="/users/new"><button>{{ $t('buttons.new') }}</button></router-link></h1>
@ -19,7 +16,7 @@
<th></th> <th></th>
</tr> </tr>
<tr v-for="user in users"> <tr v-for="user in users" :key="user.id">
<td>{{ user.username }}</td> <td>{{ user.username }}</td>
<td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td> <td><i v-if="user.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
<td>{{ user.filesystem }}</td> <td>{{ user.filesystem }}</td>
@ -44,7 +41,7 @@ export default {
api.getUsers().then(users => { api.getUsers().then(users => {
this.users = users this.users = users
}).catch(error => { }).catch(error => {
this.$store.commit('showError', error) this.$showError(error)
}) })
} }
} }

View File

@ -1,92 +0,0 @@
package filemanager
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
var defaultCredentials = "{\"username\":\"admin\",\"password\":\"admin\"}"
var authHandlerTests = []struct {
Data string
Expected int
}{
{defaultCredentials, http.StatusOK},
{"{\"username\":\"admin\",\"password\":\"wrong\"}", http.StatusForbidden},
{"{\"username\":\"wrong\",\"password\":\"admin\"}", http.StatusForbidden},
}
func TestAuthHandler(t *testing.T) {
fm := newTest(t)
defer fm.Clean()
for _, test := range authHandlerTests {
req, err := http.NewRequest("POST", "/api/auth/get", strings.NewReader(test.Data))
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
fm.ServeHTTP(w, req)
if w.Code != test.Expected {
t.Errorf("Wrong status code: got %v want %v", w.Code, test.Expected)
}
}
}
func TestRenewHandler(t *testing.T) {
fm := newTest(t)
defer fm.Clean()
// First, we have to make an auth request to get the user authenticated,
r, err := http.NewRequest("POST", "/api/auth/get", strings.NewReader(defaultCredentials))
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
fm.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Couldn't authenticate: got %v", w.Code)
}
token := w.Body.String()
// Test renew authorization via Authorization Header.
r, err = http.NewRequest("GET", "/api/auth/renew", nil)
if err != nil {
t.Fatal(err)
}
r.Header.Set("Authorization", "Bearer "+token)
w = httptest.NewRecorder()
fm.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Can't renew auth via header: got %v", w.Code)
}
// Test renew authorization via cookie field.
r, err = http.NewRequest("GET", "/api/auth/renew", nil)
if err != nil {
t.Fatal(err)
}
r.AddCookie(&http.Cookie{
Value: token,
Name: "auth",
Expires: time.Now().Add(1 * time.Hour),
})
w = httptest.NewRecorder()
fm.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Errorf("Can't renew auth via cookie: got %v", w.Code)
}
}

26
bolt/config.go Normal file
View File

@ -0,0 +1,26 @@
package bolt
import (
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
// ConfigStore is a configuration store.
type ConfigStore struct {
DB *storm.DB
}
// Get gets a configuration from the database to an interface.
func (c ConfigStore) Get(name string, to interface{}) error {
err := c.DB.Get("config", name, to)
if err == storm.ErrNotFound {
return fm.ErrNotExist
}
return err
}
// Save saves a configuration from an interface to the database.
func (c ConfigStore) Save(name string, from interface{}) error {
return c.DB.Set("config", name, from)
}

66
bolt/share.go Normal file
View File

@ -0,0 +1,66 @@
package bolt
import (
"github.com/asdine/storm"
"github.com/asdine/storm/q"
fm "github.com/hacdias/filemanager"
)
// ShareStore is a shareable links store.
type ShareStore struct {
DB *storm.DB
}
// Get gets a Share Link from an hash.
func (s ShareStore) Get(hash string) (*fm.ShareLink, error) {
var v fm.ShareLink
err := s.DB.One("Hash", hash, &v)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
return &v, err
}
// GetPermanent gets the permanent link from a path.
func (s ShareStore) GetPermanent(path string) (*fm.ShareLink, error) {
var v fm.ShareLink
err := s.DB.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&v)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
return &v, err
}
// GetByPath gets all the links for a specific path.
func (s ShareStore) GetByPath(hash string) ([]*fm.ShareLink, error) {
var v []*fm.ShareLink
err := s.DB.Find("Path", hash, &v)
if err == storm.ErrNotFound {
return v, fm.ErrNotExist
}
return v, err
}
// Gets retrieves all the shareable links.
func (s ShareStore) Gets() ([]*fm.ShareLink, error) {
var v []*fm.ShareLink
err := s.DB.All(&v)
if err == storm.ErrNotFound {
return v, fm.ErrNotExist
}
return v, err
}
// Save stores a Share Link on the database.
func (s ShareStore) Save(l *fm.ShareLink) error {
return s.DB.Save(l)
}
// Delete deletes a Share Link from the database.
func (s ShareStore) Delete(hash string) error {
return s.DB.DeleteStruct(&fm.ShareLink{Hash: hash})
}

90
bolt/users.go Normal file
View File

@ -0,0 +1,90 @@
package bolt
import (
"reflect"
"github.com/asdine/storm"
fm "github.com/hacdias/filemanager"
)
// UsersStore is a users store.
type UsersStore struct {
DB *storm.DB
}
// Get gets a user with a certain id from the database.
func (u UsersStore) Get(id int, builder fm.FSBuilder) (*fm.User, error) {
var us fm.User
err := u.DB.One("ID", id, &us)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
if err != nil {
return nil, err
}
us.FileSystem = builder(us.Scope)
return &us, nil
}
// GetByUsername gets a user with a certain username from the database.
func (u UsersStore) GetByUsername(username string, builder fm.FSBuilder) (*fm.User, error) {
var us fm.User
err := u.DB.One("Username", username, &us)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
if err != nil {
return nil, err
}
us.FileSystem = builder(us.Scope)
return &us, nil
}
// Gets gets all the users from the database.
func (u UsersStore) Gets(builder fm.FSBuilder) ([]*fm.User, error) {
var us []*fm.User
err := u.DB.All(&us)
if err == storm.ErrNotFound {
return nil, fm.ErrNotExist
}
if err != nil {
return us, err
}
for _, user := range us {
user.FileSystem = builder(user.Scope)
}
return us, err
}
// Update updates the whole user object or only certain fields.
func (u UsersStore) Update(us *fm.User, fields ...string) error {
if len(fields) == 0 {
return u.Save(us)
}
for _, field := range fields {
val := reflect.ValueOf(us).Elem().FieldByName(field).Interface()
if err := u.DB.UpdateField(us, field, val); err != nil {
return err
}
}
return nil
}
// Save saves a user to the database.
func (u UsersStore) Save(us *fm.User) error {
return u.DB.Save(us)
}
// Delete deletes a user from the database.
func (u UsersStore) Delete(id int) error {
return u.DB.DeleteStruct(&fm.User{ID: id})
}

View File

@ -8,6 +8,7 @@ import (
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
@ -32,7 +33,7 @@ func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
continue continue
} }
f.Configs[i].ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }

View File

@ -5,6 +5,7 @@ import (
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
@ -29,7 +30,7 @@ func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
continue continue
} }
f.Configs[i].ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }

View File

@ -5,6 +5,7 @@ import (
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
@ -29,7 +30,7 @@ func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
continue continue
} }
f.Configs[i].ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }

View File

@ -10,12 +10,17 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/asdine/storm"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt"
"github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
var databases = map[string]*storm.DB{}
// Parse ... // Parse ...
func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) { func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) {
var ( var (
@ -24,7 +29,7 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
) )
for c.Next() { for c.Next() {
u := filemanager.User{ u := &filemanager.User{
Locale: "en", Locale: "en",
AllowCommands: true, AllowCommands: true,
AllowEdit: true, AllowEdit: true,
@ -141,6 +146,15 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
} }
u.CSS = string(css) u.CSS = string(css)
case "view_mode":
if !c.NextArg() {
return nil, c.ArgErr()
}
u.ViewMode = c.Val()
if u.ViewMode != "mosaic" && u.ViewMode != "list" {
return nil, c.ArgErr()
}
case "no_auth": case "no_auth":
if !c.NextArg() { if !c.NextArg() {
noAuth = true noAuth = true
@ -183,13 +197,45 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n") ". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
} }
u.Scope = scope
u.FileSystem = fileutils.Dir(scope) u.FileSystem = fileutils.Dir(scope)
m, err := filemanager.New(database, u)
var db *storm.DB
if stored, ok := databases[database]; ok {
db = stored
} else {
db, err = storm.Open(database)
databases[database] = db
}
if err != nil {
return nil, err
}
m := &filemanager.FileManager{
NoAuth: noAuth,
BaseURL: "",
PrefixURL: "",
DefaultUser: u,
Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope)
},
}
err = m.Setup()
if err != nil {
return nil, err
}
switch plugin { switch plugin {
case "hugo": case "hugo":
// Initialize the default settings for Hugo. // Initialize the default settings for Hugo.
hugo := &filemanager.Hugo{ hugo := &staticgen.Hugo{
Root: scope, Root: scope,
Public: filepath.Join(scope, "public"), Public: filepath.Join(scope, "public"),
Args: []string{}, Args: []string{},
@ -197,13 +243,13 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
} }
// Attaches Hugo plugin to this file manager instance. // Attaches Hugo plugin to this file manager instance.
err = m.EnableStaticGen(hugo) err = m.Attach(hugo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "jekyll": case "jekyll":
// Initialize the default settings for Jekyll. // Initialize the default settings for Jekyll.
jekyll := &filemanager.Jekyll{ jekyll := &staticgen.Jekyll{
Root: scope, Root: scope,
Public: filepath.Join(scope, "_site"), Public: filepath.Join(scope, "_site"),
Args: []string{}, Args: []string{},
@ -211,7 +257,7 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
} }
// Attaches Hugo plugin to this file manager instance. // Attaches Hugo plugin to this file manager instance.
err = m.EnableStaticGen(jekyll) err = m.Attach(jekyll)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -10,9 +10,14 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/asdine/storm"
lumberjack "gopkg.in/natefinch/lumberjack.v2" lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt"
h "github.com/hacdias/filemanager/http"
"github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -25,8 +30,11 @@ var (
scope string scope string
commands string commands string
logfile string logfile string
staticgen string staticg string
locale string locale string
baseurl string
prefixurl string
viewMode string
port int port int
noAuth bool noAuth bool
allowCommands bool allowCommands bool
@ -34,7 +42,6 @@ var (
allowNew bool allowNew bool
allowPublish bool allowPublish bool
showVer bool showVer bool
version = "master"
) )
func init() { func init() {
@ -44,14 +51,17 @@ func init() {
flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file") flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file")
flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file") flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file")
flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users") flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users")
flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL")
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users") flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL")
flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users")
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users") flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users")
flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users") flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users")
flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users") flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users")
flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users") flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users")
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication") flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication")
flag.StringVar(&locale, "locale", "en", "Default locale for new users") flag.StringVar(&locale, "locale", "en", "Default locale for new users")
flag.StringVar(&staticgen, "staticgen", "", "Static Generator you want to enable") flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version") flag.BoolVarP(&showVer, "version", "v", false, "Show version")
} }
@ -69,6 +79,9 @@ func setupViper() {
viper.SetDefault("StaticGen", "") viper.SetDefault("StaticGen", "")
viper.SetDefault("Locale", "en") viper.SetDefault("Locale", "en")
viper.SetDefault("NoAuth", false) viper.SetDefault("NoAuth", false)
viper.SetDefault("BaseURL", "")
viper.SetDefault("PrefixURL", "")
viper.SetDefault("ViewMode", "mosaic")
viper.BindPFlag("Port", flag.Lookup("port")) viper.BindPFlag("Port", flag.Lookup("port"))
viper.BindPFlag("Address", flag.Lookup("address")) viper.BindPFlag("Address", flag.Lookup("address"))
@ -83,21 +96,16 @@ func setupViper() {
viper.BindPFlag("Locale", flag.Lookup("locale")) viper.BindPFlag("Locale", flag.Lookup("locale"))
viper.BindPFlag("StaticGen", flag.Lookup("staticgen")) viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
viper.BindPFlag("NoAuth", flag.Lookup("no-auth")) viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
viper.SetConfigName("filemanager") viper.SetConfigName("filemanager")
viper.AddConfigPath(".") viper.AddConfigPath(".")
} }
func printVersion() { func printVersion() {
version = strings.TrimSpace(version) fmt.Println("filemanager version", filemanager.Version)
if version == "" {
fmt.Println("filemanager is at an untracked version")
} else {
version = strings.TrimPrefix(version, "v")
fmt.Println("filemanager version", version)
}
os.Exit(0) os.Exit(0)
} }
@ -148,52 +156,6 @@ func main() {
}) })
} }
// Create a File Manager instance.
fm, err := filemanager.New(viper.GetString("Database"), filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
FileSystem: fileutils.Dir(viper.GetString("Scope")),
})
if viper.GetBool("NoAuth") {
fm.NoAuth = true
}
if err != nil {
log.Fatal(err)
}
switch viper.GetString("StaticGen") {
case "hugo":
hugo := &filemanager.Hugo{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{},
CleanPublic: true,
}
if err = fm.EnableStaticGen(hugo); err != nil {
log.Fatal(err)
}
case "jekyll":
jekyll := &filemanager.Jekyll{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"},
CleanPublic: true,
}
if err = fm.EnableStaticGen(jekyll); err != nil {
log.Fatal(err)
}
}
// Builds the address and a listener. // Builds the address and a listener.
laddr := viper.GetString("Address") + ":" + viper.GetString("Port") laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
listener, err := net.Listen("tcp", laddr) listener, err := net.Listen("tcp", laddr)
@ -205,7 +167,73 @@ func main() {
fmt.Println("Listening on", listener.Addr().String()) fmt.Println("Listening on", listener.Addr().String())
// Starts the server. // Starts the server.
if err := http.Serve(listener, fm); err != nil { if err := http.Serve(listener, handler()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func handler() http.Handler {
db, err := storm.Open(viper.GetString("Database"))
if err != nil {
log.Fatal(err)
}
fm := &filemanager.FileManager{
NoAuth: viper.GetBool("NoAuth"),
BaseURL: viper.GetString("BaseURL"),
PrefixURL: viper.GetString("PrefixURL"),
DefaultUser: &filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"),
CSS: "",
Scope: viper.GetString("Scope"),
FileSystem: fileutils.Dir(viper.GetString("Scope")),
ViewMode: viper.GetString("ViewMode"),
},
Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope)
},
}
err = fm.Setup()
if err != nil {
log.Fatal(err)
}
switch viper.GetString("StaticGen") {
case "hugo":
hugo := &staticgen.Hugo{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{},
CleanPublic: true,
}
if err = fm.Attach(hugo); err != nil {
log.Fatal(err)
}
case "jekyll":
jekyll := &staticgen.Jekyll{
Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"},
CleanPublic: true,
}
if err = fm.Attach(jekyll); err != nil {
log.Fatal(err)
}
}
return h.Handler(fm)
}

73
doc.go Normal file
View File

@ -0,0 +1,73 @@
/*
Package filemanager provides a web interface to access your files
wherever you are. To use this package as a middleware for your app,
you'll need to import both File Manager and File Manager HTTP packages.
import (
fm "github.com/hacdias/filemanager"
h "github.com/hacdias/filemanager/http"
)
Then, you should create a new FileManager object with your options. In this
case, I'm using BoltDB (via Storm package) as a Store. So, you'll also need
to import "github.com/hacdias/filemanager/bolt".
db, _ := storm.Open("bolt.db")
m := &fm.FileManager{
NoAuth: false,
DefaultUser: &fm.User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{"git"},
Rules: []*fm.Rule{},
Locale: "en",
CSS: "",
Scope: ".",
FileSystem: fileutils.Dir("."),
},
Store: &fm.Store{
Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db},
},
NewFS: func(scope string) fm.FileSystem {
return fileutils.Dir(scope)
},
}
The credentials for the first user are always 'admin' for both the user and
the password, and they can be changed later through the settings. The first
user is always an Admin and has all of the permissions set to 'true'.
Then, you should set the Prefix URL and the Base URL, using the following
functions:
m.SetBaseURL("/")
m.SetPrefixURL("/")
The Prefix URL is a part of the path that is already stripped from the
r.URL.Path variable before the request arrives to File Manager's handler.
This is a function that will rarely be used. You can see one example on Caddy
filemanager plugin.
The Base URL is the URL path where you want File Manager to be available in. If
you want to be available at the root path, you should call:
m.SetBaseURL("/")
But if you want to access it at '/admin', you would call:
m.SetBaseURL("/admin")
Now, that you already have a File Manager instance created, you just need to
add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
One simple implementation for this, at port 80, in the root of the domain, would be:
http.ListenAndServe(":80", h.Handler(m))
*/
package filemanager

72
file.go
View File

@ -7,7 +7,6 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"errors"
"hash" "hash"
"io" "io"
"io/ioutil" "io/ioutil"
@ -23,13 +22,9 @@ import (
"github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/parser"
) )
var ( // File contains the information about a particular file or directory.
errInvalidOption = errors.New("Invalid option") type File struct {
) // Indicates the Kind of view on the front-end (Listing, editor or preview).
// file contains the information about a particular file or directory.
type file struct {
// Indicates the Kind of view on the front-end (listing, editor or preview).
Kind string `json:"kind"` Kind string `json:"kind"`
// The name of the file. // The name of the file.
Name string `json:"name"` Name string `json:"name"`
@ -54,37 +49,35 @@ type file struct {
// Stores the content of a text file. // Stores the content of a text file.
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
*listing `json:",omitempty"` *Listing `json:",omitempty"`
Metadata string `json:"metadata,omitempty"` Metadata string `json:"metadata,omitempty"`
Language string `json:"language,omitempty"` Language string `json:"language,omitempty"`
} }
// A listing is the context used to fill out a template. // A Listing is the context used to fill out a template.
type listing struct { type Listing struct {
// The items (files and folders) in the path. // The items (files and folders) in the path.
Items []*file `json:"items"` Items []*File `json:"items"`
// The number of directories in the listing. // The number of directories in the Listing.
NumDirs int `json:"numDirs"` NumDirs int `json:"numDirs"`
// The number of files (items that aren't directories) in the listing. // The number of files (items that aren't directories) in the Listing.
NumFiles int `json:"numFiles"` NumFiles int `json:"numFiles"`
// Which sorting order is used. // Which sorting order is used.
Sort string `json:"sort"` Sort string `json:"sort"`
// And which order. // And which order.
Order string `json:"order"` Order string `json:"order"`
// Displays in mosaic or list.
Display string `json:"display"`
} }
// getInfo gets the file information and, in case of error, returns the // GetInfo gets the file information and, in case of error, returns the
// respective HTTP error code // respective HTTP error code
func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) { func GetInfo(url *url.URL, c *FileManager, u *User) (*File, error) {
var err error var err error
i := &file{ i := &File{
URL: "/files" + url.String(), URL: "/files" + url.String(),
VirtualPath: url.Path, VirtualPath: url.Path,
Path: filepath.Join(string(u.FileSystem), url.Path), Path: filepath.Join(u.Scope, url.Path),
} }
info, err := u.FileSystem.Stat(url.Path) info, err := u.FileSystem.Stat(url.Path)
@ -106,11 +99,11 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
return i, nil return i, nil
} }
// getListing gets the information about a specific directory and its files. // GetListing gets the information about a specific directory and its files.
func (i *file) getListing(c *RequestContext, r *http.Request) error { func (i *File) GetListing(u *User, r *http.Request) error {
// Gets the directory information using the Virtual File System of // Gets the directory information using the Virtual File System of
// the user configuration. // the user configuration.
f, err := c.User.FileSystem.OpenFile(c.File.VirtualPath, os.O_RDONLY, 0) f, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
if err != nil { if err != nil {
return err return err
} }
@ -123,7 +116,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
} }
var ( var (
fileinfos []*file fileinfos []*File
dirCount, fileCount int dirCount, fileCount int
) )
@ -134,7 +127,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
for _, f := range files { for _, f := range files {
name := f.Name() name := f.Name()
allowed := c.User.Allowed("/" + name) allowed := u.Allowed("/" + name)
if !allowed { if !allowed {
continue continue
@ -150,7 +143,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
// Absolute URL // Absolute URL
url := url.URL{Path: baseurl + name} url := url.URL{Path: baseurl + name}
i := &file{ i := &File{
Name: f.Name(), Name: f.Name(),
Size: f.Size(), Size: f.Size(),
ModTime: f.ModTime(), ModTime: f.ModTime(),
@ -166,7 +159,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
fileinfos = append(fileinfos, i) fileinfos = append(fileinfos, i)
} }
i.listing = &listing{ i.Listing = &Listing{
Items: fileinfos, Items: fileinfos,
NumDirs: dirCount, NumDirs: dirCount,
NumFiles: fileCount, NumFiles: fileCount,
@ -175,8 +168,8 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
return nil return nil
} }
// getEditor gets the editor based on a Info struct // GetEditor gets the editor based on a Info struct
func (i *file) getEditor() error { func (i *File) GetEditor() error {
i.Language = editorLanguage(i.Extension) i.Language = editorLanguage(i.Extension)
// If the editor will hold only content, leave now. // If the editor will hold only content, leave now.
if editorMode(i.Language) == "content" { if editorMode(i.Language) == "content" {
@ -205,7 +198,7 @@ func (i *file) getEditor() error {
// GetFileType obtains the mimetype and converts it to a simple // GetFileType obtains the mimetype and converts it to a simple
// type nomenclature. // type nomenclature.
func (i *file) GetFileType(checkContent bool) error { func (i *File) GetFileType(checkContent bool) error {
var content []byte var content []byte
var err error var err error
@ -283,7 +276,8 @@ End:
return nil return nil
} }
func (i file) Checksum(kind string) (string, error) { // Checksum retrieves the checksum of a file.
func (i File) Checksum(algo string) (string, error) {
file, err := os.Open(i.Path) file, err := os.Open(i.Path)
if err != nil { if err != nil {
return "", err return "", err
@ -293,7 +287,7 @@ func (i file) Checksum(kind string) (string, error) {
var h hash.Hash var h hash.Hash
switch kind { switch algo {
case "md5": case "md5":
h = md5.New() h = md5.New()
case "sha1": case "sha1":
@ -303,7 +297,7 @@ func (i file) Checksum(kind string) (string, error) {
case "sha512": case "sha512":
h = sha512.New() h = sha512.New()
default: default:
return "", errInvalidOption return "", ErrInvalidOption
} }
_, err = io.Copy(h, file) _, err = io.Copy(h, file)
@ -315,12 +309,12 @@ func (i file) Checksum(kind string) (string, error) {
} }
// CanBeEdited checks if the extension of a file is supported by the editor // CanBeEdited checks if the extension of a file is supported by the editor
func (i file) CanBeEdited() bool { func (i File) CanBeEdited() bool {
return i.Type == "text" return i.Type == "text"
} }
// ApplySort applies the sort order using .Order and .Sort // ApplySort applies the sort order using .Order and .Sort
func (l listing) ApplySort() { func (l Listing) ApplySort() {
// Check '.Order' to know how to sort // Check '.Order' to know how to sort
if l.Order == "desc" { if l.Order == "desc" {
switch l.Sort { switch l.Sort {
@ -349,10 +343,10 @@ func (l listing) ApplySort() {
} }
} }
// Implement sorting for listing // Implement sorting for Listing
type byName listing type byName Listing
type bySize listing type bySize Listing
type byModified listing type byModified Listing
// By Name // By Name
func (l byName) Len() int { func (l byName) Len() int {

View File

@ -1,59 +1,7 @@
// Package filemanager provides a web interface to access your files
// wherever you are. To use this package as a middleware for your app,
// you'll need to create a filemanager instance:
//
// m, err := filemanager.New(database, user)
//
// Where 'user' contains the default options for new users. You can just
// use 'filemanager.DefaultUser' or create yourself a default user:
//
// m, err := filemanager.New(database, filemanager.User{
// Admin: false,
// AllowCommands: false,
// AllowEdit: true,
// AllowNew: true,
// Commands: []string{
// "git",
// },
// Rules: []*filemanager.Rule{},
// CSS: "",
// FileSystem: webdav.Dir("/path/to/files"),
// })
//
// The credentials for the first user are always 'admin' for both the user and
// the password, and they can be changed later through the settings. The first
// user is always an Admin and has all of the permissions set to 'true'.
//
// Then, you should set the Prefix URL and the Base URL, using the following
// functions:
//
// m.SetBaseURL("/")
// m.SetPrefixURL("/")
//
// The Prefix URL is a part of the path that is already stripped from the
// r.URL.Path variable before the request arrives to File Manager's handler.
// This is a function that will rarely be used. You can see one example on Caddy
// filemanager plugin.
//
// The Base URL is the URL path where you want File Manager to be available in. If
// you want to be available at the root path, you should call:
//
// m.SetBaseURL("/")
//
// But if you want to access it at '/admin', you would call:
//
// m.SetBaseURL("/admin")
//
// Now, that you already have a File Manager instance created, you just need to
// add it to your handlers using m.ServeHTTP which is compatible to http.Handler.
// We also have a m.ServeWithErrorsHTTP that returns the status code and an error.
//
// One simple implementation for this, at port 80, in the root of the domain, would be:
//
// http.ListenAndServe(":80", m)
package filemanager package filemanager
import ( import (
"crypto/rand"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@ -65,38 +13,44 @@ import (
"strings" "strings"
"time" "time"
"golang.org/x/crypto/bcrypt"
rice "github.com/GeertJohan/go.rice" rice "github.com/GeertJohan/go.rice"
"github.com/asdine/storm"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/robfig/cron" "github.com/robfig/cron"
) )
// Version is the current File Manager version.
const Version = "(untracked)"
var ( var (
errUserExist = errors.New("user already exists") ErrExist = errors.New("the resource already exists")
errUserNotExist = errors.New("user does not exist") ErrNotExist = errors.New("the resource does not exist")
errEmptyRequest = errors.New("request body is empty") ErrEmptyRequest = errors.New("request body is empty")
errEmptyPassword = errors.New("password is empty") ErrEmptyPassword = errors.New("password is empty")
errEmptyUsername = errors.New("username is empty") ErrEmptyUsername = errors.New("username is empty")
errEmptyScope = errors.New("scope is empty") ErrEmptyScope = errors.New("scope is empty")
errWrongDataType = errors.New("wrong data type") ErrWrongDataType = errors.New("wrong data type")
errInvalidUpdateField = errors.New("invalid field to update") ErrInvalidUpdateField = errors.New("invalid field to update")
ErrInvalidOption = errors.New("Invalid option")
) )
// FileManager is a file manager instance. It should be creating using the // FileManager is a file manager instance. It should be creating using the
// 'New' function and not directly. // 'New' function and not directly.
type FileManager struct { type FileManager struct {
// The BoltDB database for this instance. // Cron job to manage schedulings.
db *storm.DB Cron *cron.Cron
// The key used to sign the JWT tokens. // The key used to sign the JWT tokens.
key []byte Key []byte
// The static assets. // The static assets.
assets *rice.Box Assets *rice.Box
// Job cron. // The Store is used to manage users, shareable links and
cron *cron.Cron // other stuff that is saved on the database.
Store *Store
// PrefixURL is a part of the URL that is already trimmed from the request URL before it // PrefixURL is a part of the URL that is already trimmed from the request URL before it
// arrives to our handlers. It may be useful when using File Manager as a middleware // arrives to our handlers. It may be useful when using File Manager as a middleware
@ -112,139 +66,56 @@ type FileManager struct {
// there will only exist one user, called "admin". // there will only exist one user, called "admin".
NoAuth bool NoAuth bool
// staticgen is the name of the current static website generator.
staticgen string
// StaticGen is the static websit generator handler. // StaticGen is the static websit generator handler.
StaticGen StaticGen StaticGen StaticGen
// The Default User needed to build the New User page. // The Default User needed to build the New User page.
DefaultUser *User DefaultUser *User
// Users is a map with the different configurations for each user.
Users map[string]*User
// A map of events to a slice of commands. // A map of events to a slice of commands.
Commands map[string][]string Commands map[string][]string
// NewFS should build a new file system for a given path.
NewFS FSBuilder
} }
// Command is a command function. // Command is a command function.
type Command func(r *http.Request, m *FileManager, u *User) error type Command func(r *http.Request, m *FileManager, u *User) error
// User contains the configuration for each user. // FSBuilder is the File System Builder.
type User struct { type FSBuilder func(scope string) FileSystem
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// Username is the user username used to login. // Setup loads the configuration from the database and configures
Username string `json:"username" storm:"index,unique"` // the Assets and the Cron job. It must always be run after
// creating a File Manager object.
// The hashed password. This never reaches the front-end because it's temporarily func (m *FileManager) Setup() error {
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// FileSystem is the virtual file system the user has access.
FileSystem fileutils.Dir `json:"filesystem"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
}
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
// DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
FileSystem: fileutils.Dir("."),
}
// New creates a new File Manager instance. If 'database' file already
// exists, it will load the users from there. Otherwise, a new user
// will be created using the 'base' variable. The 'base' User should
// not have the Password field hashed.
func New(database string, base User) (*FileManager, error) {
// Creates a new File Manager instance with the Users // Creates a new File Manager instance with the Users
// map and Assets box. // map and Assets box.
m := &FileManager{ m.Assets = rice.MustFindBox("./assets/dist")
Users: map[string]*User{}, m.Cron = cron.New()
cron: cron.New(),
assets: rice.MustFindBox("./assets/dist"),
}
// Tries to open a database on the location provided. This
// function will automatically create a new one if it doesn't
// exist.
db, err := storm.Open(database)
if err != nil {
return nil, err
}
// Tries to get the encryption key from the database. // Tries to get the encryption key from the database.
// If it doesn't exist, create a new one of 256 bits. // If it doesn't exist, create a new one of 256 bits.
err = db.Get("config", "key", &m.key) err := m.Store.Config.Get("key", &m.Key)
if err != nil && err == storm.ErrNotFound { if err != nil && err == ErrNotExist {
var bytes []byte var bytes []byte
bytes, err = generateRandomBytes(64) bytes, err = GenerateRandomBytes(64)
if err != nil { if err != nil {
return nil, err return err
} }
m.key = bytes m.Key = bytes
err = db.Set("config", "key", m.key) err = m.Store.Config.Save("key", m.Key)
} }
if err != nil { if err != nil {
return nil, err return err
} }
// Tries to get the event commands from the database. // Tries to get the event commands from the database.
// If they don't exist, initialize them. // If they don't exist, initialize them.
err = db.Get("config", "commands", &m.Commands) err = m.Store.Config.Get("commands", &m.Commands)
if err != nil && err == storm.ErrNotFound { if err != nil && err == ErrNotExist {
m.Commands = map[string][]string{ m.Commands = map[string][]string{
"before_save": {}, "before_save": {},
"after_save": {}, "after_save": {},
@ -255,35 +126,29 @@ func New(database string, base User) (*FileManager, error) {
"after_upload": {}, "after_upload": {},
"after_delete": {}, "after_delete": {},
} }
err = db.Set("config", "commands", m.Commands) err = m.Store.Config.Save("commands", m.Commands)
} }
if err != nil { if err != nil {
return nil, err return err
} }
// Tries to fetch the users from the database and if there are // Tries to fetch the users from the database.
// any, add them to the current File Manager instance. users, err := m.Store.Users.Gets(m.NewFS)
var users []User if err != nil && err != ErrNotExist {
err = db.All(&users) return err
if err != nil {
return nil, err
}
for i := range users {
m.Users[users[i].Username] = &users[i]
} }
// If there are no users in the database, it creates a new one // If there are no users in the database, it creates a new one
// based on 'base' User that must be provided by the function caller. // based on 'base' User that must be provided by the function caller.
if len(users) == 0 { if len(users) == 0 {
u := base u := *m.DefaultUser
u.Username = "admin" u.Username = "admin"
// Hashes the password. // Hashes the password.
u.Password, err = hashPassword("admin") u.Password, err = HashPassword("admin")
if err != nil { if err != nil {
return nil, err return err
} }
// The first user must be an administrator. // The first user must be an administrator.
@ -294,25 +159,18 @@ func New(database string, base User) (*FileManager, error) {
u.AllowPublish = true u.AllowPublish = true
// Saves the user to the database. // Saves the user to the database.
if err := db.Save(&u); err != nil { if err := m.Store.Users.Save(&u); err != nil {
return nil, err return err
}
} }
m.Users[u.Username] = &u m.DefaultUser.Username = ""
} m.DefaultUser.Password = ""
// Attaches db to this File Manager instance. m.Cron.AddFunc("@hourly", m.ShareCleaner)
m.db = db m.Cron.Start()
// Create the default user, making a copy of the base. return nil
base.Username = ""
base.Password = ""
m.DefaultUser = &base
m.cron.AddFunc("@hourly", m.shareCleaner)
m.cron.Start()
return m, nil
} }
// RootURL returns the actual URL where // RootURL returns the actual URL where
@ -339,95 +197,32 @@ func (m *FileManager) SetBaseURL(url string) {
m.BaseURL = strings.TrimSuffix(url, "/") m.BaseURL = strings.TrimSuffix(url, "/")
} }
// ServeHTTP handles the request. // Attach attaches a static generator to the current File Manager.
func (m *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (m *FileManager) Attach(s StaticGen) error {
code, err := serveHTTP(&RequestContext{ if reflect.TypeOf(s).Kind() != reflect.Ptr {
FileManager: m,
User: nil,
File: nil,
}, w, r)
if code >= 400 {
w.WriteHeader(code)
if err == nil {
txt := http.StatusText(code)
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
w.Write([]byte(txt))
}
}
if err != nil {
log.Print(err)
w.Write([]byte(err.Error()))
}
}
// EnableStaticGen attaches a static generator to the current File Manager
// instance.
func (m *FileManager) EnableStaticGen(data StaticGen) error {
if reflect.TypeOf(data).Kind() != reflect.Ptr {
return errors.New("data should be a pointer to interface, not interface") return errors.New("data should be a pointer to interface, not interface")
} }
if h, ok := data.(*Hugo); ok { err := s.Setup()
return m.enableHugo(h) if err != nil {
}
if j, ok := data.(*Jekyll); ok {
return m.enableJekyll(j)
}
return errors.New("unknown static website generator")
}
func (m *FileManager) enableHugo(h *Hugo) error {
if err := h.find(); err != nil {
return err return err
} }
m.staticgen = "hugo" m.StaticGen = s
m.StaticGen = h
err := m.db.Get("staticgen", "hugo", h) err = m.Store.Config.Get("staticgen_"+s.Name(), s)
if err != nil && err == storm.ErrNotFound { if err == ErrNotExist {
err = m.db.Set("staticgen", "hugo", *h) return m.Store.Config.Save("staticgen_"+s.Name(), s)
} }
return nil
}
func (m *FileManager) enableJekyll(j *Jekyll) error {
if err := j.find(); err != nil {
return err return err
} }
if len(j.Args) == 0 { // ShareCleaner removes sharing links that are no longer active.
j.Args = []string{"build"}
}
if j.Args[0] != "build" {
j.Args = append([]string{"build"}, j.Args...)
}
m.staticgen = "jekyll"
m.StaticGen = j
err := m.db.Get("staticgen", "jekyll", j)
if err != nil && err == storm.ErrNotFound {
err = m.db.Set("staticgen", "jekyll", *j)
}
return nil
}
// shareCleaner removes sharing links that are no longer active.
// This function is set to run periodically. // This function is set to run periodically.
func (m FileManager) shareCleaner() { func (m FileManager) ShareCleaner() {
var links []shareLink
// Get all links. // Get all links.
err := m.db.All(&links) links, err := m.Store.Share.Gets()
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return return
@ -436,7 +231,7 @@ func (m FileManager) shareCleaner() {
// Find the expired ones. // Find the expired ones.
for i := range links { for i := range links {
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) { if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
err = m.db.DeleteStruct(&links[i]) err = m.Store.Share.Delete(links[i].Hash)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
@ -444,37 +239,6 @@ func (m FileManager) shareCleaner() {
} }
} }
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule
i := len(u.Rules) - 1
for i >= 0 {
rule = u.Rules[i]
if rule.Regex {
if rule.Regexp.MatchString(url) {
return rule.Allow
}
} else if strings.HasPrefix(url, rule.Path) {
return rule.Allow
}
i--
}
return true
}
// MatchString checks if this string matches the regular expression.
func (r *Regexp) MatchString(s string) bool {
if r.regexp == nil {
r.regexp = regexp.MustCompile(r.Raw)
}
return r.regexp.MatchString(s)
}
// Runner runs the commands for a certain event type. // Runner runs the commands for a certain event type.
func (m FileManager) Runner(event string, path string, destination string, user *User) error { func (m FileManager) Runner(event string, path string, destination string, user *User) error {
commands := []string{} commands := []string{}
@ -531,3 +295,217 @@ func (m FileManager) Runner(event string, path string, destination string, user
return nil return nil
} }
// DefaultUser is used on New, when no 'base' user is provided.
var DefaultUser = User{
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
AllowPublish: true,
LockPassword: false,
Commands: []string{},
Rules: []*Rule{},
CSS: "",
Admin: true,
Locale: "en",
Scope: ".",
FileSystem: fileutils.Dir("."),
ViewMode: "mosaic",
}
// User contains the configuration for each user.
type User struct {
// ID is the required primary key with auto increment0
ID int `storm:"id,increment"`
// Username is the user username used to login.
Username string `json:"username" storm:"index,unique"`
// The hashed password. This never reaches the front-end because it's temporarily
// emptied during JSON marshall.
Password string `json:"password"`
// Tells if this user is an admin.
Admin bool `json:"admin"`
// Scope is the path the user has access to.
Scope string `json:"filesystem"`
// FileSystem is the virtual file system the user has access.
FileSystem FileSystem `json:"-"`
// Rules is an array of access and deny rules.
Rules []*Rule `json:"rules"`
// Custom styles for this user.
CSS string `json:"css"`
// Locale is the language of the user.
Locale string `json:"locale"`
// Prevents the user to change its password.
LockPassword bool `json:"lockPassword"`
// These indicate if the user can perform certain actions.
AllowNew bool `json:"allowNew"` // Create files and folders
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
// Commands is the list of commands the user can execute.
Commands []string `json:"commands"`
// User view mode for files and folders.
ViewMode string `json:"viewMode"`
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule
i := len(u.Rules) - 1
for i >= 0 {
rule = u.Rules[i]
if rule.Regex {
if rule.Regexp.MatchString(url) {
return rule.Allow
}
} else if strings.HasPrefix(url, rule.Path) {
return rule.Allow
}
i--
}
return true
}
// Rule is a dissalow/allow rule.
type Rule struct {
// Regex indicates if this rule uses Regular Expressions or not.
Regex bool `json:"regex"`
// Allow indicates if this is an allow rule. Set 'false' to be a disallow rule.
Allow bool `json:"allow"`
// Path is the corresponding URL path for this rule.
Path string `json:"path"`
// Regexp is the regular expression. Only use this when 'Regex' was set to true.
Regexp *Regexp `json:"regexp"`
}
// Regexp is a regular expression wrapper around native regexp.
type Regexp struct {
Raw string `json:"raw"`
regexp *regexp.Regexp
}
// MatchString checks if this string matches the regular expression.
func (r *Regexp) MatchString(s string) bool {
if r.regexp == nil {
r.regexp = regexp.MustCompile(r.Raw)
}
return r.regexp.MatchString(s)
}
// ShareLink is the information needed to build a shareable link.
type ShareLink struct {
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
Expires bool `json:"expires"`
ExpireDate time.Time `json:"expireDate"`
}
// Store is a collection of the stores needed to get
// and save information.
type Store struct {
Users UsersStore
Config ConfigStore
Share ShareStore
}
// UsersStore is the interface to manage users.
type UsersStore interface {
Get(id int, builder FSBuilder) (*User, error)
GetByUsername(username string, builder FSBuilder) (*User, error)
Gets(builder FSBuilder) ([]*User, error)
Save(u *User) error
Update(u *User, fields ...string) error
Delete(id int) error
}
// ConfigStore is the interface to manage configuration.
type ConfigStore interface {
Get(name string, to interface{}) error
Save(name string, from interface{}) error
}
// ShareStore is the interface to manage share links.
type ShareStore interface {
Get(hash string) (*ShareLink, error)
GetPermanent(path string) (*ShareLink, error)
GetByPath(path string) ([]*ShareLink, error)
Gets() ([]*ShareLink, error)
Save(s *ShareLink) error
Delete(hash string) error
}
// StaticGen is a static website generator.
type StaticGen interface {
SettingsPath() string
Name() string
Setup() error
Hook(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *Context, w http.ResponseWriter, r *http.Request) (int, error)
}
// FileSystem is the interface to work with the file system.
type FileSystem interface {
Mkdir(name string, perm os.FileMode) error
OpenFile(name string, flag int, perm os.FileMode) (*os.File, error)
RemoveAll(name string) error
Rename(oldName, newName string) error
Stat(name string) (os.FileInfo, error)
Copy(src, dst string) error
}
// Context contains the needed information to make handlers work.
type Context struct {
*FileManager
User *User
File *File
// On API handlers, Router is the APi handler we want.
Router string
}
// HashPassword generates an hash from a password using bcrypt.
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPasswordHash compares a password with an hash to check if they match.
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// GenerateRandomBytes returns securely generated random bytes.
// It will return an fm.Error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}

View File

@ -1,49 +0,0 @@
package filemanager
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hacdias/fileutils"
)
type test struct {
*FileManager
Temp string
}
func (t test) Clean() {
t.db.Close()
os.RemoveAll(t.Temp)
}
func newTest(t *testing.T) *test {
temp, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatalf("Error creating temporary directory: %v", err)
}
scope := filepath.Join(temp, "scope")
database := filepath.Join(temp, "database.db")
err = fileutils.CopyDir("./testdata", scope)
if err != nil {
t.Fatalf("Error copying the test data: %v", err)
}
user := DefaultUser
user.FileSystem = fileutils.Dir(scope)
fm, err := New(database, user)
if err != nil {
t.Fatalf("Error creating a file manager instance: %v", err)
}
return &test{
FileManager: fm,
Temp: temp,
}
}

View File

@ -1,27 +1,25 @@
package filemanager package http
import ( import (
"crypto/rand"
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"golang.org/x/crypto/bcrypt"
jwt "github.com/dgrijalva/jwt-go" jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request" "github.com/dgrijalva/jwt-go/request"
fm "github.com/hacdias/filemanager"
) )
// authHandler proccesses the authentication for the user. // authHandler proccesses the authentication for the user.
func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// NoAuth instances shouldn't call this method. // NoAuth instances shouldn't call this method.
if c.NoAuth { if c.NoAuth {
return 0, nil return 0, nil
} }
// Receive the credentials from the request and unmarshal them. // Receive the credentials from the request and unmarshal them.
var cred User var cred fm.User
if r.Body == nil { if r.Body == nil {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -32,13 +30,13 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
} }
// Checks if the user exists. // Checks if the user exists.
u, ok := c.Users[cred.Username] u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
if !ok { if err != nil {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
// Checks if the password is correct. // Checks if the password is correct.
if !checkPasswordHash(cred.Password, u.Password) { if !fm.CheckPasswordHash(cred.Password, u.Password) {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -48,7 +46,7 @@ func authHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int
// renewAuthHandler is used when the front-end already has a JWT token // renewAuthHandler is used when the front-end already has a JWT token
// and is checking if it is up to date. If so, updates its info. // and is checking if it is up to date. If so, updates its info.
func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func renewAuthHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
ok, u := validateAuth(c, r) ok, u := validateAuth(c, r)
if !ok { if !ok {
return http.StatusForbidden, nil return http.StatusForbidden, nil
@ -60,23 +58,21 @@ func renewAuthHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// claims is the JWT claims. // claims is the JWT claims.
type claims struct { type claims struct {
User fm.User
NoAuth bool `json:"noAuth"`
jwt.StandardClaims jwt.StandardClaims
} }
// printToken prints the final JWT token to the user. // printToken prints the final JWT token to the user.
func printToken(c *RequestContext, w http.ResponseWriter) (int, error) { func printToken(c *fm.Context, w http.ResponseWriter) (int, error) {
// Creates a copy of the user and removes it password // Creates a copy of the user and removes it password
// hash so it never arrives to the user. // hash so it never arrives to the user.
u := User{} u := fm.User{}
u = *c.User u = *c.User
u.Password = "" u.Password = ""
// Builds the claims. // Builds the claims.
claims := claims{ claims := claims{
u, u,
c.NoAuth,
jwt.StandardClaims{ jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
Issuer: "File Manager", Issuer: "File Manager",
@ -85,7 +81,7 @@ func printToken(c *RequestContext, w http.ResponseWriter) (int, error) {
// Creates the token and signs it. // Creates the token and signs it.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(c.key) signed, err := token.SignedString(c.Key)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
@ -119,14 +115,14 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
// validateAuth is used to validate the authentication and returns the // validateAuth is used to validate the authentication and returns the
// User if it is valid. // User if it is valid.
func validateAuth(c *RequestContext, r *http.Request) (bool, *User) { func validateAuth(c *fm.Context, r *http.Request) (bool, *fm.User) {
if c.NoAuth { if c.NoAuth {
c.User = c.DefaultUser c.User = c.DefaultUser
return true, c.User return true, c.User
} }
keyFunc := func(token *jwt.Token) (interface{}, error) { keyFunc := func(token *jwt.Token) (interface{}, error) {
return c.key, nil return c.Key, nil
} }
var claims claims var claims claims
token, err := request.ParseFromRequestWithClaims(r, token, err := request.ParseFromRequestWithClaims(r,
@ -139,38 +135,11 @@ func validateAuth(c *RequestContext, r *http.Request) (bool, *User) {
return false, nil return false, nil
} }
u, ok := c.Users[claims.User.Username] u, err := c.Store.Users.Get(claims.User.ID, c.NewFS)
if !ok { if err != nil {
return false, nil return false, nil
} }
c.User = u c.User = u
return true, u return true, u
} }
// hashPassword generates an hash from a password using bcrypt.
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// checkPasswordHash compares a password with an hash to check if they match.
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// generateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"io" "io"
@ -9,13 +9,14 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
"github.com/mholt/archiver" "github.com/mholt/archiver"
) )
// downloadHandler creates an archive in one of the supported formats (zip, tar, // downloadHandler creates an archive in one of the supported formats (zip, tar,
// tar.gz or tar.bz2) and sends it to be downloaded. // tar.gz or tar.bz2) and sends it to be downloaded.
func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func downloadHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("format") query := r.URL.Query().Get("format")
// If the file isn't a directory, serve it using http.ServeFile. We display it // If the file isn't a directory, serve it using http.ServeFile. We display it
@ -24,7 +25,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
if r.URL.Query().Get("inline") == "true" { if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline") w.Header().Set("Content-Disposition", "inline")
} else { } else {
w.Header().Set("Content-Disposition", "attachment; filename="+c.File.Name) w.Header().Set("Content-Disposition", "attachment; filename=\""+c.File.Name+"\"")
} }
http.ServeFile(w, r, c.File.Path) http.ServeFile(w, r, c.File.Path)
@ -106,7 +107,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
defer file.Close() defer file.Close()
w.Header().Set("Content-Disposition", "attachment; filename="+name) w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
_, err = io.Copy(w, file) _, err = io.Copy(w, file)
return 0, err return 0, err
} }

View File

@ -1,29 +1,48 @@
package filemanager package http
import ( import (
"encoding/json" "encoding/json"
"html/template" "html/template"
"log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/asdine/storm" fm "github.com/hacdias/filemanager"
) )
// RequestContext contains the needed information to make handlers work. // Handler returns a function compatible with http.HandleFunc.
type RequestContext struct { func Handler(m *fm.FileManager) http.Handler {
*FileManager return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
User *User code, err := serve(&fm.Context{
File *file FileManager: m,
// On API handlers, Router is the APi handler we want. User: nil,
Router string File: nil,
}, w, r)
if code >= 400 {
w.WriteHeader(code)
if err == nil {
txt := http.StatusText(code)
log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
w.Write([]byte(txt))
}
} }
// serveHTTP is the main entry point of this HTML application. if err != nil {
func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { log.Print(err)
w.Write([]byte(err.Error()))
}
})
}
// serve is the main entry point of this HTML application.
func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Checks if the URL contains the baseURL and strips it. Otherwise, it just // Checks if the URL contains the baseURL and strips it. Otherwise, it just
// returns a 404 error because we're not supposed to be here! // returns a 404 fm.Error because we're not supposed to be here!
p := strings.TrimPrefix(r.URL.Path, c.BaseURL) p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
if len(p) >= len(r.URL.Path) && c.BaseURL != "" { if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
@ -35,11 +54,7 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
// Check if this request is made to the service worker. If so, // Check if this request is made to the service worker. If so,
// pass it through a template to add the needed variables. // pass it through a template to add the needed variables.
if r.URL.Path == "/sw.js" { if r.URL.Path == "/sw.js" {
return renderFile( return renderFile(c, w, "sw.js")
c, w,
c.assets.MustString("sw.js"),
"application/javascript",
)
} }
// Checks if this request is made to the static assets folder. If so, and // Checks if this request is made to the static assets folder. If so, and
@ -77,29 +92,21 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
w.Header().Set("x-content-type", "nosniff") w.Header().Set("x-content-type", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block") w.Header().Set("x-xss-protection", "1; mode=block")
return renderFile( return renderFile(c, w, "index.html")
c, w,
c.assets.MustString("index.html"),
"text/html",
)
} }
// staticHandler handles the static assets path. // staticHandler handles the static assets path.
func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" { if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r) http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
return renderFile( return renderFile(c, w, "static/manifest.json")
c, w,
c.assets.MustString("static/manifest.json"),
"application/json",
)
} }
// apiHandler is the main entry point for the /api endpoint. // apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth/get" { if r.URL.Path == "/auth/get" {
return authHandler(c, w, r) return authHandler(c, w, r)
} }
@ -135,9 +142,9 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
if c.Router == "checksum" || c.Router == "download" { if c.Router == "checksum" || c.Router == "download" {
var err error var err error
c.File, err = getInfo(r.URL, c.FileManager, c.User) c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
} }
@ -169,11 +176,11 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
} }
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512. // serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
func checksumHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo") query := r.URL.Query().Get("algo")
val, err := c.File.Checksum(query) val, err := c.File.Checksum(query)
if err == errInvalidOption { if err == fm.ErrInvalidOption {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} else if err != nil { } else if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
@ -201,14 +208,35 @@ func splitURL(path string) (string, string) {
} }
// renderFile renders a file using a template with some needed variables. // renderFile renders a file using a template with some needed variables.
func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentType string) (int, error) { func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) {
tpl := template.Must(template.New("file").Parse(file)) tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file)))
var contentType string
switch filepath.Ext(file) {
case ".html":
contentType = "text/html"
case ".js":
contentType = "application/javascript"
case ".json":
contentType = "application/json"
default:
contentType = "text"
}
w.Header().Set("Content-Type", contentType+"; charset=utf-8") w.Header().Set("Content-Type", contentType+"; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{ data := map[string]interface{}{
"BaseURL": c.RootURL(), "BaseURL": c.RootURL(),
"StaticGen": c.staticgen, "NoAuth": c.NoAuth,
}) "Version": fm.Version,
}
if c.StaticGen != nil {
data["StaticGen"] = c.StaticGen.Name()
}
err := tpl.Execute(w, data)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -216,15 +244,12 @@ func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentTy
return 0, nil return 0, nil
} }
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { // sharePage build the share page.
var s shareLink func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
err := c.db.One("Hash", r.URL.Path, &s) s, err := c.Store.Share.Get(r.URL.Path)
if err == storm.ErrNotFound { if err == fm.ErrNotExist {
return renderFile( w.WriteHeader(http.StatusNotFound)
c, w, return renderFile(c, w, "static/share/404.html")
c.assets.MustString("static/share/404.html"),
"text/html",
)
} }
if err != nil { if err != nil {
@ -232,22 +257,19 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
} }
if s.Expires && s.ExpireDate.Before(time.Now()) { if s.Expires && s.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&s) c.Store.Share.Delete(s.Hash)
return renderFile( w.WriteHeader(http.StatusNotFound)
c, w, return renderFile(c, w, "static/share/404.html")
c.assets.MustString("static/share/404.html"),
"text/html",
)
} }
r.URL.Path = s.Path r.URL.Path = s.Path
info, err := os.Stat(s.Path) info, err := os.Stat(s.Path)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
c.File = &file{ c.File = &fm.File{
Path: s.Path, Path: s.Path,
Name: info.Name(), Name: info.Name(),
ModTime: info.ModTime(), ModTime: info.ModTime(),
@ -259,7 +281,7 @@ func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
dl := r.URL.Query().Get("dl") dl := r.URL.Query().Get("dl")
if dl == "" || dl == "0" { if dl == "" || dl == "0" {
tpl := template.Must(template.New("file").Parse(c.assets.MustString("static/share/index.html"))) tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html")))
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{ err := tpl.Execute(w, map[string]interface{}{
@ -299,8 +321,8 @@ func matchURL(first, second string) bool {
return strings.HasPrefix(first, second) return strings.HasPrefix(first, second)
} }
// errorToHTTP converts errors to HTTP Status Code. // ErrorToHTTP converts errors to HTTP Status Code.
func errorToHTTP(err error, gone bool) int { func ErrorToHTTP(err error, gone bool) int {
switch { switch {
case err == nil: case err == nil:
return http.StatusOK return http.StatusOK

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"errors" "errors"
@ -13,6 +13,7 @@ import (
"strings" "strings"
"time" "time"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
) )
@ -26,7 +27,7 @@ func sanitizeURL(url string) string {
return path return path
} }
func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path) r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method { switch r.Method {
@ -36,7 +37,7 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return resourceDeleteHandler(c, w, r) return resourceDeleteHandler(c, w, r)
case http.MethodPut: case http.MethodPut:
// Before save command handler. // Before save command handler.
path := filepath.Join(string(c.User.FileSystem), r.URL.Path) path := filepath.Join(c.User.Scope, r.URL.Path)
if err := c.Runner("before_save", path, "", c.User); err != nil { if err := c.Runner("before_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -61,11 +62,11 @@ func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusNotImplemented, nil return http.StatusNotImplemented, nil
} }
func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file. // Gets the information of the directory/file.
f, err := getInfo(r.URL, c.FileManager, c.User) f, err := fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// If it's a dir and the path doesn't end with a trailing slash, // If it's a dir and the path doesn't end with a trailing slash,
@ -82,7 +83,7 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
// Tries to get the file type. // Tries to get the file type.
if err = f.GetFileType(true); err != nil { if err = f.GetFileType(true); err != nil {
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
// Serve a preview if the file can't be edited or the // Serve a preview if the file can't be edited or the
@ -96,23 +97,23 @@ func resourceGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
f.Kind = "editor" f.Kind = "editor"
// Tries to get the editor data. // Tries to get the editor data.
if err = f.getEditor(); err != nil { if err = f.GetEditor(); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return renderJSON(w, f) return renderJSON(w, f)
} }
func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.File f := c.File
f.Kind = "listing" f.Kind = "listing"
// Tries to get the listing data. // Tries to get the listing data.
if err := f.getListing(c, r); err != nil { if err := f.GetListing(c.User, r); err != nil {
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
listing := f.listing listing := f.Listing
// Defines the cookie scope. // Defines the cookie scope.
cookieScope := c.RootURL() cookieScope := c.RootURL()
@ -129,12 +130,10 @@ func listingHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (
} }
listing.ApplySort() listing.ApplySort()
listing.Display = displayMode(w, r, cookieScope)
return renderJSON(w, f) return renderJSON(w, f)
} }
func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Prevent the removal of the root directory. // Prevent the removal of the root directory.
if r.URL.Path == "/" || !c.User.AllowEdit { if r.URL.Path == "/" || !c.User.AllowEdit {
return http.StatusForbidden, nil return http.StatusForbidden, nil
@ -143,7 +142,7 @@ func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Req
// Remove the file or folder. // Remove the file or folder.
err := c.User.FileSystem.RemoveAll(r.URL.Path) err := c.User.FileSystem.RemoveAll(r.URL.Path)
if err != nil { if err != nil {
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
// Fire the trigger // Fire the trigger
@ -154,7 +153,7 @@ func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Req
return http.StatusOK, nil return http.StatusOK, nil
} }
func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowNew && r.Method == http.MethodPost { if !c.User.AllowNew && r.Method == http.MethodPost {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -179,7 +178,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
// Otherwise we try to create the directory. // Otherwise we try to create the directory.
err := c.User.FileSystem.Mkdir(r.URL.Path, 0776) err := c.User.FileSystem.Mkdir(r.URL.Path, 0776)
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// If using POST method, we are trying to create a new file so it is not // If using POST method, we are trying to create a new file so it is not
@ -194,20 +193,20 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
// Create/Open the file. // Create/Open the file.
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776) f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
defer f.Close() defer f.Close()
// Copies the new content for the file. // Copies the new content for the file.
_, err = io.Copy(f, r.Body) _, err = io.Copy(f, r.Body)
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// Gets the info about the file. // Gets the info about the file.
fi, err := f.Stat() fi, err := f.Stat()
if err != nil { if err != nil {
return errorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// Check if this instance has a Static Generator and handles publishing // Check if this instance has a Static Generator and handles publishing
@ -231,7 +230,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
return http.StatusOK, nil return http.StatusOK, nil
} }
func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
publish := r.Header.Get("Publish") publish := r.Header.Get("Publish")
schedule := r.Header.Get("Schedule") schedule := r.Header.Get("Schedule")
@ -252,7 +251,7 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
c.cron.AddFunc(t.Format("05 04 15 02 01 *"), func() { c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
_, err := resourcePublish(c, w, r) _, err := resourcePublish(c, w, r)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@ -262,8 +261,8 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
return http.StatusOK, nil return http.StatusOK, nil
} }
func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path) path := filepath.Join(c.User.Scope, r.URL.Path)
// Before save command handler. // Before save command handler.
if err := c.Runner("before_publish", path, "", c.User); err != nil { if err := c.Runner("before_publish", path, "", c.User); err != nil {
@ -284,7 +283,7 @@ func resourcePublish(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// resourcePatchHandler is the entry point for resource handler. // resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit { if !c.User.AllowEdit {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -293,7 +292,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
action := r.Header.Get("Action") action := r.Header.Get("Action")
dst, err := url.QueryUnescape(dst) dst, err := url.QueryUnescape(dst)
if err != nil { if err != nil {
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
src := r.URL.Path src := r.URL.Path
@ -316,33 +315,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ
} }
} }
return errorToHTTP(err, true), err return ErrorToHTTP(err, true), err
}
// displayMode obtains the display mode from the Cookie.
func displayMode(w http.ResponseWriter, r *http.Request, scope string) string {
var displayMode string
// Checks the cookie.
if displayCookie, err := r.Cookie("display"); err == nil {
displayMode = displayCookie.Value
}
// If it's invalid, set it to mosaic, which is the default.
if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
displayMode = "mosaic"
}
// Set the cookie.
http.SetCookie(w, &http.Cookie{
Name: "display",
Value: displayMode,
MaxAge: 31536000,
Path: scope,
Secure: r.TLS != nil,
})
return displayMode
} }
// handleSortOrder gets and stores for a Listing the 'sort' and 'order', // handleSortOrder gets and stores for a Listing the 'sort' and 'order',

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"bytes" "bytes"
@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"reflect" "reflect"
fm "github.com/hacdias/filemanager"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@ -26,7 +27,7 @@ type option struct {
func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) { func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request body is empty. // Checks if the request body is empty.
if r.Body == nil { if r.Body == nil {
return nil, errEmptyRequest return nil, fm.ErrEmptyRequest
} }
// Parses the request body and checks if it's well formed. // Parses the request body and checks if it's well formed.
@ -38,13 +39,13 @@ func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) {
// Checks if the request type is right. // Checks if the request type is right.
if mod.What != "settings" { if mod.What != "settings" {
return nil, errWrongDataType return nil, fm.ErrWrongDataType
} }
return mod, nil return mod, nil
} }
func settingsHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func settingsHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "" && r.URL.Path != "/" { if r.URL.Path != "" && r.URL.Path != "/" {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
@ -64,7 +65,7 @@ type settingsGetRequest struct {
StaticGen []option `json:"staticGen"` StaticGen []option `json:"staticGen"`
} }
func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func settingsGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin { if !c.User.Admin {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -93,7 +94,7 @@ func settingsGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return renderJSON(w, result) return renderJSON(w, result)
} }
func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func settingsPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.Admin { if !c.User.Admin {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
@ -102,9 +103,10 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
if err != nil { if err != nil {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
// Update the commands. // Update the commands.
if mod.Which == "commands" { if mod.Which == "commands" {
if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil { if err := c.Store.Config.Save("commands", mod.Data.Commands); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -119,7 +121,7 @@ func settingsPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
err = c.db.Set("staticgen", c.staticgen, c.StaticGen) err = c.Store.Config.Save("staticgen_"+c.StaticGen.Name(), c.StaticGen)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"encoding/hex" "encoding/hex"
@ -8,18 +8,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/asdine/storm" fm "github.com/hacdias/filemanager"
"github.com/asdine/storm/q"
) )
type shareLink struct { func shareHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
Expires bool `json:"expires"`
ExpireDate time.Time `json:"expireDate"`
}
func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path) r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method { switch r.Method {
@ -34,14 +26,10 @@ func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
return http.StatusNotImplemented, nil return http.StatusNotImplemented, nil
} }
func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
var ( path := filepath.Join(c.User.Scope, r.URL.Path)
s []*shareLink s, err := c.Store.Share.GetByPath(path)
path = filepath.Join(string(c.User.FileSystem), r.URL.Path) if err == fm.ErrNotExist {
)
err := c.db.Find("Path", path, &s)
if err == storm.ErrNotFound {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
@ -51,37 +39,42 @@ func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
for i, link := range s { for i, link := range s {
if link.Expires && link.ExpireDate.Before(time.Now()) { if link.Expires && link.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&shareLink{Hash: link.Hash}) c.Store.Share.Delete(link.Hash)
s = append(s[:i], s[i+1:]...) s = append(s[:i], s[i+1:]...)
} }
} }
if len(s) == 0 {
return http.StatusNotFound, nil
}
return renderJSON(w, s) return renderJSON(w, s)
} }
func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path) path := filepath.Join(c.User.Scope, r.URL.Path)
var s shareLink var s *fm.ShareLink
expire := r.URL.Query().Get("expires") expire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit") unit := r.URL.Query().Get("unit")
if expire == "" { if expire == "" {
err := c.db.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&s) var err error
s, err = c.Store.Share.GetPermanent(path)
if err == nil { if err == nil {
w.Write([]byte(c.RootURL() + "/share/" + s.Hash)) w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
return 0, nil return 0, nil
} }
} }
bytes, err := generateRandomBytes(32) bytes, err := fm.GenerateRandomBytes(32)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
str := hex.EncodeToString(bytes) str := hex.EncodeToString(bytes)
s = shareLink{ s = &fm.ShareLink{
Path: path, Path: path,
Hash: str, Hash: str,
Expires: expire != "", Expires: expire != "",
@ -108,19 +101,16 @@ func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
s.ExpireDate = time.Now().Add(add) s.ExpireDate = time.Now().Add(add)
} }
err = c.db.Save(&s) if err := c.Store.Share.Save(s); err != nil {
if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return renderJSON(w, s) return renderJSON(w, s)
} }
func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink s, err := c.Store.Share.Get(strings.TrimPrefix(r.URL.Path, "/"))
if err == fm.ErrNotExist {
err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)
if err == storm.ErrNotFound {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
@ -128,7 +118,7 @@ func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
err = c.db.DeleteStruct(&s) err = c.Store.Share.Delete(s.Hash)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"encoding/json" "encoding/json"
@ -9,7 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/asdine/storm" fm "github.com/hacdias/filemanager"
) )
type modifyRequest struct { type modifyRequest struct {
@ -19,12 +19,12 @@ type modifyRequest struct {
type modifyUserRequest struct { type modifyUserRequest struct {
*modifyRequest *modifyRequest
Data *User `json:"data"` Data *fm.User `json:"data"`
} }
// usersHandler is the entry point of the users API. It's just a router // usersHandler is the entry point of the users API. It's just a router
// to send the request to its // to send the request to its
func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func usersHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If the user isn't admin and isn't making a PUT // If the user isn't admin and isn't making a PUT
// request, then return forbidden. // request, then return forbidden.
if !c.User.Admin && r.Method != http.MethodPut { if !c.User.Admin && r.Method != http.MethodPut {
@ -47,7 +47,7 @@ func usersHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (in
// getUserID returns the id from the user which is present // getUserID returns the id from the user which is present
// in the request url. If the url is invalid and doesn't // in the request url. If the url is invalid and doesn't
// contain a valid ID, it returns an error. // contain a valid ID, it returns an fm.Error.
func getUserID(r *http.Request) (int, error) { func getUserID(r *http.Request) (int, error) {
// Obtains the ID in string from the URL and converts // Obtains the ID in string from the URL and converts
// it into an integer. // it into an integer.
@ -63,11 +63,11 @@ func getUserID(r *http.Request) (int, error) {
// getUser returns the user which is present in the request // getUser returns the user which is present in the request
// body. If the body is empty or the JSON is invalid, it // body. If the body is empty or the JSON is invalid, it
// returns an error. // returns an fm.Error.
func getUser(r *http.Request) (*User, string, error) { func getUser(c *fm.Context, r *http.Request) (*fm.User, string, error) {
// Checks if the request body is empty. // Checks if the request body is empty.
if r.Body == nil { if r.Body == nil {
return nil, "", errEmptyRequest return nil, "", fm.ErrEmptyRequest
} }
// Parses the request body and checks if it's well formed. // Parses the request body and checks if it's well formed.
@ -79,13 +79,14 @@ func getUser(r *http.Request) (*User, string, error) {
// Checks if the request type is right. // Checks if the request type is right.
if mod.What != "user" { if mod.What != "user" {
return nil, "", errWrongDataType return nil, "", fm.ErrWrongDataType
} }
mod.Data.FileSystem = c.NewFS(mod.Data.Scope)
return mod.Data, mod.Which, nil return mod.Data, mod.Which, nil
} }
func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func usersGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Request for the default user data. // Request for the default user data.
if r.URL.Path == "/base" { if r.URL.Path == "/base" {
return renderJSON(w, c.DefaultUser) return renderJSON(w, c.DefaultUser)
@ -93,15 +94,15 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Request for the listing of users. // Request for the listing of users.
if r.URL.Path == "/" { if r.URL.Path == "/" {
users := []User{} users, err := c.Store.Users.Gets(c.NewFS)
if err != nil {
return http.StatusInternalServerError, err
}
for _, user := range c.Users { for _, u := range users {
// Copies the user info and removes its // Removes the user password so it won't
// password so it won't be sent to the // be sent to the front-end.
// front-end.
u := *user
u.Password = "" u.Password = ""
users = append(users, u)
} }
sort.Slice(users, func(i, j int) bool { sort.Slice(users, func(i, j int) bool {
@ -116,54 +117,47 @@ func usersGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Searches for the user and prints the one who matches. u, err := c.Store.Users.Get(id, c.NewFS)
for _, user := range c.Users { if err == fm.ErrExist {
if user.ID != id { return http.StatusNotFound, err
continue }
if err != nil {
return http.StatusInternalServerError, err
} }
u := *user
u.Password = "" u.Password = ""
return renderJSON(w, u) return renderJSON(w, u)
} }
// If there aren't any matches, return not found. func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
return http.StatusNotFound, errUserNotExist
}
func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/" { if r.URL.Path != "/" {
return http.StatusMethodNotAllowed, nil return http.StatusMethodNotAllowed, nil
} }
u, _, err := getUser(r) u, _, err := getUser(c, r)
if err != nil { if err != nil {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
// Checks if username isn't empty. // Checks if username isn't empty.
if u.Username == "" { if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername return http.StatusBadRequest, fm.ErrEmptyUsername
} }
// Checks if filesystem isn't empty. // Checks if scope isn't empty.
if u.FileSystem == "" { if u.Scope == "" {
return http.StatusBadRequest, errEmptyScope return http.StatusBadRequest, fm.ErrEmptyScope
} }
// Checks if password isn't empty. // Checks if password isn't empty.
if u.Password == "" { if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword return http.StatusBadRequest, fm.ErrEmptyPassword
}
// The username, password and scope cannot be empty.
if u.Username == "" || u.Password == "" || u.FileSystem == "" {
return http.StatusBadRequest, errors.New("username, password or scope is empty")
} }
// Initialize rules if they're not initialized. // Initialize rules if they're not initialized.
if u.Rules == nil { if u.Rules == nil {
u.Rules = []*Rule{} u.Rules = []*fm.Rule{}
} }
// Initialize commands if not initialized. // Initialize commands if not initialized.
@ -177,12 +171,12 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// Checks if the scope exists. // Checks if the scope exists.
if code, err := checkFS(string(u.FileSystem)); err != nil { if code, err := checkFS(u.Scope); err != nil {
return code, err return code, err
} }
// Hashes the password. // Hashes the password.
pw, err := hashPassword(u.Password) pw, err := fm.HashPassword(u.Password)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -190,18 +184,15 @@ func usersPostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
u.Password = pw u.Password = pw
// Saves the user to the database. // Saves the user to the database.
err = c.db.Save(u) err = c.Store.Users.Save(u)
if err == storm.ErrAlreadyExists { if err == fm.ErrExist {
return http.StatusConflict, errUserExist return http.StatusConflict, err
} }
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Saves the user to the memory.
c.Users[u.Username] = u
// Set the Location header and return. // Set the Location header and return.
w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID)) w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID))
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
@ -231,7 +222,7 @@ func checkFS(path string) (int, error) {
return 0, nil return 0, nil
} }
func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func usersDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/" { if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil return http.StatusMethodNotAllowed, nil
} }
@ -242,27 +233,19 @@ func usersDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Reques
} }
// Deletes the user from the database. // Deletes the user from the database.
err = c.db.DeleteStruct(&User{ID: id}) err = c.Store.Users.Delete(id)
if err == storm.ErrNotFound { if err == fm.ErrNotExist {
return http.StatusNotFound, errUserNotExist return http.StatusNotFound, fm.ErrNotExist
} }
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Delete the user from the in-memory users map.
for _, user := range c.Users {
if user.ID == id {
delete(c.Users, user.Username)
break
}
}
return http.StatusOK, nil return http.StatusOK, nil
} }
func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// New users should be created on /api/users. // New users should be created on /api/users.
if r.URL.Path == "/" { if r.URL.Path == "/" {
return http.StatusMethodNotAllowed, nil return http.StatusMethodNotAllowed, nil
@ -280,7 +263,7 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// Gets the user from the request body. // Gets the user from the request body.
u, which, err := getUser(r) u, which, err := getUser(c, r)
if err != nil { if err != nil {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
@ -289,12 +272,9 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
if which == "partial" { if which == "partial" {
c.User.CSS = u.CSS c.User.CSS = u.CSS
c.User.Locale = u.Locale c.User.Locale = u.Locale
err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS) c.User.ViewMode = u.ViewMode
if err != nil {
return http.StatusInternalServerError, err
}
err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale) err = c.Store.Users.Update(c.User, "CSS", "Locale", "ViewMode")
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -305,16 +285,19 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Updates the Password. // Updates the Password.
if which == "password" { if which == "password" {
if u.Password == "" { if u.Password == "" {
return http.StatusBadRequest, errEmptyPassword return http.StatusBadRequest, fm.ErrEmptyPassword
} }
pw, err := hashPassword(u.Password) if id == c.User.ID && c.User.LockPassword {
return http.StatusForbidden, nil
}
c.User.Password, err = fm.HashPassword(u.Password)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
c.User.Password = pw err = c.Store.Users.Update(c.User, "Password")
err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -324,27 +307,27 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// If can only be all. // If can only be all.
if which != "all" { if which != "all" {
return http.StatusBadRequest, errInvalidUpdateField return http.StatusBadRequest, fm.ErrInvalidUpdateField
} }
// Checks if username isn't empty. // Checks if username isn't empty.
if u.Username == "" { if u.Username == "" {
return http.StatusBadRequest, errEmptyUsername return http.StatusBadRequest, fm.ErrEmptyUsername
} }
// Checks if filesystem isn't empty. // Checks if filesystem isn't empty.
if u.FileSystem == "" { if u.Scope == "" {
return http.StatusBadRequest, errEmptyScope return http.StatusBadRequest, fm.ErrEmptyScope
} }
// Checks if the scope exists. // Checks if the scope exists.
if code, err := checkFS(string(u.FileSystem)); err != nil { if code, err := checkFS(u.Scope); err != nil {
return code, err return code, err
} }
// Initialize rules if they're not initialized. // Initialize rules if they're not initialized.
if u.Rules == nil { if u.Rules == nil {
u.Rules = []*Rule{} u.Rules = []*fm.Rule{}
} }
// Initialize commands if not initialized. // Initialize commands if not initialized.
@ -353,22 +336,20 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// Gets the current saved user from the in-memory map. // Gets the current saved user from the in-memory map.
var suser *User suser, err := c.Store.Users.Get(id, c.NewFS)
for _, user := range c.Users { if err == fm.ErrNotExist {
if user.ID == id {
suser = user
break
}
}
if suser == nil {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
if err != nil {
return http.StatusInternalServerError, err
}
u.ID = id u.ID = id
// Changes the password if the request wants it. // Changes the password if the request wants it.
if u.Password != "" { if u.Password != "" {
pw, err := hashPassword(u.Password) pw, err := fm.HashPassword(u.Password)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@ -380,17 +361,10 @@ func usersPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
// Updates the whole User struct because we always are supposed // Updates the whole User struct because we always are supposed
// to send a new entire object. // to send a new entire object.
err = c.db.Save(u) err = c.Store.Users.Update(u)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// If the user changed the username, delete the old user
// from the in-memory user map.
if suser.Username != u.Username {
delete(c.Users, suser.Username)
}
c.Users[u.Username] = u
return http.StatusOK, nil return http.StatusOK, nil
} }

View File

@ -1,4 +1,4 @@
package filemanager package http
import ( import (
"bytes" "bytes"
@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
fm "github.com/hacdias/filemanager"
) )
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
@ -26,8 +27,8 @@ var (
) )
// command handles the requests for VCS related commands: git, svn and mercurial // command handles the requests for VCS related commands: git, svn and mercurial
func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for errors. // Upgrades the connection to a websocket and checks for fm.Errors.
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
return 0, err return 0, err
@ -81,7 +82,7 @@ func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, er
} }
// Gets the path and initializes a buffer. // Gets the path and initializes a buffer.
path := string(c.User.FileSystem) + "/" + r.URL.Path path := c.User.Scope + "/" + r.URL.Path
path = filepath.Clean(path) path = filepath.Clean(path)
buff := new(bytes.Buffer) buff := new(bytes.Buffer)
@ -91,7 +92,7 @@ func command(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, er
cmd.Stderr = buff cmd.Stderr = buff
cmd.Stdout = buff cmd.Stdout = buff
// Starts the command and checks for errors. // Starts the command and checks for fm.Errors.
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
@ -239,8 +240,8 @@ func parseSearch(value string) *searchOptions {
} }
// search searches for a file or directory. // search searches for a file or directory.
func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for errors. // Upgrades the connection to a websocket and checks for fm.Errors.
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
return 0, err return 0, err
@ -269,7 +270,7 @@ func search(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, err
search = parseSearch(value) search = parseSearch(value)
scope := strings.TrimPrefix(r.URL.Path, "/") scope := strings.TrimPrefix(r.URL.Path, "/")
scope = "/" + scope scope = "/" + scope
scope = string(c.User.FileSystem) + scope scope = c.User.Scope + scope
scope = strings.Replace(scope, "\\", "/", -1) scope = strings.Replace(scope, "\\", "/", -1)
scope = filepath.Clean(scope) scope = filepath.Clean(scope)

9161
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"filesize": "^3.5.10", "filesize": "^3.5.10",
"moment": "^2.18.1", "moment": "^2.18.1",
"normalize.css": "^7.0.0", "normalize.css": "^7.0.0",
"noty": "^3.1.2",
"vue": "^2.3.3", "vue": "^2.3.3",
"vue-i18n": "^7.1.0", "vue-i18n": "^7.1.0",
"vue-router": "^2.7.0", "vue-router": "^2.7.0",

24
publish.sh Normal file
View File

@ -0,0 +1,24 @@
#!/bin/bash
echo "Building assets"
./build.sh
echo "Updating version number to $1..."
sed -i "s|(untracked)|$1|g" filemanager.go
echo "Commiting..."
git add -A
git commit -m "Version $1"
git push
echo "Creating the tag..."
git tag "v$1"
git push --tags
echo "Commiting untracked version notice..."
sed -i "s|$1|(untracked)|g" filemanager.go
git add -A
git commit -m "[ci skip] auto: setting untracked version"
git push
echo "Done!"

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
package filemanager package staticgen
import ( import (
"errors" "errors"
@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
fm "github.com/hacdias/filemanager"
"github.com/hacdias/varutils" "github.com/hacdias/varutils"
) )
@ -17,15 +18,6 @@ var (
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
) )
// StaticGen is a static website generator.
type StaticGen interface {
SettingsPath() string
Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error)
}
// Hugo is the Hugo static website generator. // Hugo is the Hugo static website generator.
type Hugo struct { type Hugo struct {
// Website root // Website root
@ -66,8 +58,13 @@ func (h Hugo) SettingsPath() string {
return "/config." + frontmatter return "/config." + frontmatter
} }
// Name is the plugin's name.
func (h Hugo) Name() string {
return "hugo"
}
// Hook is the pre-api handler. // Hook is the pre-api handler.
func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func (h Hugo) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// If we are not using HTTP Post, we shall return Method Not Allowed // If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method. // since we are only working with this method.
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
@ -87,7 +84,7 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path) filename := filepath.Join(c.User.Scope, r.URL.Path)
archetype := r.Header.Get("archetype") archetype := r.Header.Get("archetype")
ext := filepath.Ext(filename) ext := filepath.Ext(filename)
@ -110,8 +107,8 @@ func (h Hugo) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
} }
// Publish publishes a post. // Publish publishes a post.
func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path) filename := filepath.Join(c.User.Scope, r.URL.Path)
// We only run undraft command if it is a file. // We only run undraft command if it is a file.
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") { if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
@ -127,7 +124,7 @@ func (h Hugo) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request)
} }
// Preview handles the preview path. // Preview handles the preview path.
func (h *Hugo) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none. // Get a new temporary path if there is none.
if h.previewPath == "" { if h.previewPath == "" {
path, err := ioutil.TempDir("", "") path, err := ioutil.TempDir("", "")
@ -186,7 +183,8 @@ func (h Hugo) undraft(file string) error {
return nil return nil
} }
func (h *Hugo) find() error { // Setup sets up the plugin.
func (h *Hugo) Setup() error {
var err error var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil { if h.Exe, err = exec.LookPath("hugo"); err != nil {
return err return err
@ -194,114 +192,3 @@ func (h *Hugo) find() error {
return nil return nil
} }
// Jekyll is the Jekyll static website generator.
type Jekyll struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Jekyll executable path
Exe string `name:"Executable"`
// Jekyll arguments
Args []string `name:"Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// SettingsPath retrieves the correct settings path.
func (j Jekyll) SettingsPath() string {
return "/_config.yml"
}
// Hook is the pre-api handler.
func (j Jekyll) Hook(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
// Publish publishes a post.
func (j Jekyll) Publish(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(string(c.User.FileSystem), r.URL.Path)
// We only run undraft command if it is a file.
if err := j.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
// Regenerates the file
j.run()
return 0, nil
}
// Preview handles the preview path.
func (j *Jekyll) Preview(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if j.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
j.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := j.Args
args = append(args, "--baseurl", c.RootURL()+"/preview/")
args = append(args, "--drafts")
args = append(args, "--destination", j.previewPath)
// Builds the preview.
if err := runCommand(j.Exe, args, j.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
return 0, nil
}
func (j Jekyll) run() {
// If the CleanPublic option is enabled, clean it.
if j.CleanPublic {
os.RemoveAll(j.Public)
}
if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
log.Println(err)
}
}
func (j Jekyll) undraft(file string) error {
if !strings.Contains(file, "_drafts") {
return nil
}
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
}
func (j *Jekyll) find() error {
var err error
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
return err
}
return nil
}
// runCommand executes an external command
func runCommand(command string, args []string, path string) error {
cmd := exec.Command(command, args...)
cmd.Dir = path
out, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(out))
}
return nil
}

125
staticgen/jekyll.go Normal file
View File

@ -0,0 +1,125 @@
package staticgen
import (
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
fm "github.com/hacdias/filemanager"
)
// Jekyll is the Jekyll static website generator.
type Jekyll struct {
// Website root
Root string `name:"Website Root"`
// Public folder
Public string `name:"Public Directory"`
// Jekyll executable path
Exe string `name:"Executable"`
// Jekyll arguments
Args []string `name:"Arguments"`
// Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview
previewPath string
}
// Name is the plugin's name.
func (j Jekyll) Name() string {
return "jekyll"
}
// SettingsPath retrieves the correct settings path.
func (j Jekyll) SettingsPath() string {
return "/_config.yml"
}
// Hook is the pre-api handler.
func (j Jekyll) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}
// Publish publishes a post.
func (j Jekyll) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(c.User.Scope, r.URL.Path)
// We only run undraft command if it is a file.
if err := j.undraft(filename); err != nil {
return http.StatusInternalServerError, err
}
// Regenerates the file
j.run()
return 0, nil
}
// Preview handles the preview path.
func (j *Jekyll) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none.
if j.previewPath == "" {
path, err := ioutil.TempDir("", "")
if err != nil {
return http.StatusInternalServerError, err
}
j.previewPath = path
}
// Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination.
args := j.Args
args = append(args, "--baseurl", c.RootURL()+"/preview/")
args = append(args, "--drafts")
args = append(args, "--destination", j.previewPath)
// Builds the preview.
if err := runCommand(j.Exe, args, j.Root); err != nil {
return http.StatusInternalServerError, err
}
// Serves the temporary path with the preview.
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
return 0, nil
}
func (j Jekyll) run() {
// If the CleanPublic option is enabled, clean it.
if j.CleanPublic {
os.RemoveAll(j.Public)
}
if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
log.Println(err)
}
}
func (j Jekyll) undraft(file string) error {
if !strings.Contains(file, "_drafts") {
return nil
}
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
}
// Setup sets up the plugin.
func (j *Jekyll) Setup() error {
var err error
if j.Exe, err = exec.LookPath("jekyll"); err != nil {
return err
}
if len(j.Args) == 0 {
j.Args = []string{"build"}
}
if j.Args[0] != "build" {
j.Args = append([]string{"build"}, j.Args...)
}
return nil
}

19
staticgen/staticgen.go Normal file
View File

@ -0,0 +1,19 @@
package staticgen
import (
"errors"
"os/exec"
)
// runCommand executes an external command
func runCommand(command string, args []string, path string) error {
cmd := exec.Command(command, args...)
cmd.Dir = path
out, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(out))
}
return nil
}

0
testdata/.gitkeep vendored
View File