change new folder and file permissions #190

progresses on sharing #192

Progresses on #192

Progresses on #192

Little API update

Build assets
This commit is contained in:
Henrique Dias 2017-08-11 09:33:47 +01:00
parent 7ee721a9e3
commit 8684343605
No known key found for this signature in database
GPG Key ID: 936F5EB68D786730
38 changed files with 1072 additions and 379 deletions

View File

@ -9,7 +9,7 @@
<title>File Manager</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="/static/img/icons/favicon.ico"><![endif]-->
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
<!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
<meta name="theme-color" content="#2979ff">

View File

@ -29,6 +29,7 @@
<!-- Menu that shows on listing AND mobile when there are files selected -->
<div id="file-selection" v-if="isMobile && req.kind === 'listing'">
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
<share-button v-show="showRenameButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showMoveButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
@ -38,6 +39,7 @@
<!-- This buttons are shown on a dropdown on mobile phones -->
<div id="dropdown" :class="{ active: showMore }">
<div v-if="!isListing || !isMobile">
<share-button v-show="showRenameButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showMoveButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
@ -74,6 +76,7 @@ import SwitchButton from './buttons/SwitchView'
import MoveButton from './buttons/Move'
import CopyButton from './buttons/Copy'
import ScheduleButton from './buttons/Schedule'
import ShareButton from './buttons/Share'
import {mapGetters, mapState} from 'vuex'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
@ -84,6 +87,7 @@ export default {
Search,
InfoButton,
DeleteButton,
ShareButton,
RenameButton,
DownloadButton,
CopyButton,

View File

@ -82,7 +82,7 @@
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'search',

View File

@ -8,7 +8,7 @@
<script>
import {mapGetters, mapState} from 'vuex'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'download-button',

View File

@ -0,0 +1,17 @@
<template>
<button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action">
<i class="material-icons">share</i>
<span>{{ $t('buttons.share') }}</span>
</button>
</template>
<script>
export default {
name: 'share-button',
methods: {
show (event) {
this.$store.commit('showHover', 'share')
}
}
}
</script>

View File

@ -11,7 +11,7 @@
<script>
import { mapState } from 'vuex'
import CodeMirror from '@/utils/codemirror'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {
@ -129,7 +129,7 @@ export default {
api.put(this.$route.path, content, regenerate, this.schedule)
.then(() => {
buttons.done(button)
buttons.success(button)
this.$store.commit('setSchedule', '')
})
.catch(error => {

View File

@ -91,7 +91,7 @@
import {mapState} from 'vuex'
import Item from './ListingItem'
import css from '@/utils/css'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {
@ -325,7 +325,7 @@ export default {
Promise.all(promises)
.then(() => {
buttons.done('upload')
buttons.success('upload')
this.$store.commit('setReload', true)
})
.catch(error => {

View File

@ -33,7 +33,7 @@
import { mapMutations, mapGetters, mapState } from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'item',

View File

@ -38,7 +38,7 @@
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
import InfoButton from '@/components/buttons/Info'
import DeleteButton from '@/components/buttons/Delete'
import RenameButton from '@/components/buttons/Rename'

View File

@ -21,7 +21,7 @@
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {
@ -51,7 +51,7 @@ export default {
// Execute the promises.
api.copy(items)
.then(() => {
buttons.done('copy')
buttons.success('copy')
this.$router.push({ path: this.dest })
})
.catch(error => {

View File

@ -17,7 +17,7 @@
<script>
import {mapGetters, mapMutations, mapState} from 'vuex'
import api from '@/utils/api'
import { remove } from '@/utils/api'
import url from '@/utils/url'
import buttons from '@/utils/buttons'
@ -36,9 +36,9 @@ export default {
// If we are not on a listing, delete the current
// opened file.
if (this.req.kind !== 'listing') {
api.delete(this.$route.path)
remove(this.$route.path)
.then(() => {
buttons.done('delete')
buttons.success('delete')
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
})
.catch(error => {
@ -59,12 +59,12 @@ export default {
let promises = []
for (let index of this.selected) {
promises.push(api.delete(this.req.items[index].url))
promises.push(remove(this.req.items[index].url))
}
Promise.all(promises)
.then(() => {
buttons.done('delete')
buttons.success('delete')
this.$store.commit('setReload', true)
})
.catch(error => {

View File

@ -13,7 +13,7 @@
<script>
import {mapGetters, mapState} from 'vuex'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'download',

View File

@ -19,7 +19,7 @@
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'file-list',

View File

@ -34,7 +34,7 @@
import {mapState, mapGetters} from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'info',

View File

@ -21,7 +21,7 @@
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import api from '@/utils/api'
import * as api from '@/utils/api'
import buttons from '@/utils/buttons'
export default {
@ -51,7 +51,7 @@ export default {
// Execute the promises.
api.move(items)
.then(() => {
buttons.done('move')
buttons.success('move')
this.$router.push({ path: this.dest })
})
.catch(error => {

View File

@ -18,7 +18,7 @@
<script>
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'new-dir',

View File

@ -18,7 +18,7 @@
<script>
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'new-file',

View File

@ -14,6 +14,7 @@
<replace v-else-if="showReplace"></replace>
<schedule v-else-if="show === 'schedule'"></schedule>
<new-archetype v-else-if="show === 'new-archetype'"></new-archetype>
<share v-else-if="show === 'share'"></share>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</template>
@ -33,9 +34,10 @@ import NewDir from './NewDir'
import NewArchetype from './NewArchetype'
import Replace from './Replace'
import Schedule from './Schedule'
import Share from './Share'
import { mapState } from 'vuex'
import buttons from '@/utils/buttons'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'prompts',
@ -50,6 +52,7 @@ export default {
Success,
Move,
Copy,
Share,
NewFile,
NewDir,
Help,

View File

@ -20,7 +20,7 @@
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'rename',

View File

@ -0,0 +1,153 @@
<template>
<div class="prompt" id="share">
<h3>{{ $t('buttons.share') }}</h3>
<p></p>
<ul>
<li v-if="!hasPermanent">
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
</li>
<li v-for="link in links" :key="link.hash">
<a :href="buildLink(link.hash)" target="_blank">
<template v-if="link.expires">{{ humanTime(link.expireDate) }}</template>
<template v-else>{{ $t('permanent') }}</template>
</a>
<button class="action"
@click="deleteLink($event, link)"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
<button class="action copy"
:data-clipboard-text="buildLink(link.hash)"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
</li>
<li>
<input autofocus
type="number"
max="2147483647"
min="0"
@keyup.enter="submit"
v-model.trim="time">
<select v-model="unit" :aria-label="$t('time.unit')">
<option value="seconds">{{ $t('time.seconds') }}</option>
<option value="minutes">{{ $t('time.minutes') }}</option>
<option value="hours">{{ $t('time.hours') }}</option>
<option value="days">{{ $t('time.days') }}</option>
</select>
<button class="action"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
</li>
</ul>
<div>
<button class="cancel"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { getShare, deleteShare, share } from '@/utils/api'
import moment from 'moment'
import Clipboard from 'clipboard'
export default {
name: 'share',
data: function () {
return {
time: '',
unit: 'hours',
hasPermanent: false,
links: [],
clip: null
}
},
computed: {
...mapState([ 'baseURL', 'req', 'selected', 'selectedCount' ]),
url () {
// Get the current name of the file we are editing.
if (this.req.kind !== 'listing') {
return this.$route.path
}
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return
}
return this.req.items[this.selected[0]].url
}
},
beforeMount () {
getShare(this.url)
.then(links => {
this.links = links
this.sort()
for (let link of this.links) {
if (!link.expires) {
this.hasPermanent = true
break
}
}
})
.catch(error => {
if (error === 404) return
this.showError(error)
})
},
mounted () {
this.clip = new Clipboard('.copy')
},
methods: {
...mapMutations([ 'showError' ]),
submit: function (event) {
if (!this.time) return
share(this.url, this.time, this.unit)
.then(result => { this.links.push(result); this.sort() })
.catch(error => { this.showError(error) })
},
getPermalink (event) {
share(this.url)
.then(result => {
this.links.push(result)
this.sort()
this.hasPermanent = true
})
.catch(error => { this.showError(error) })
},
deleteLink (event, link) {
event.preventDefault()
deleteShare(link.hash)
.then(() => {
if (!link.expires) this.hasPermanent = false
this.links = this.links.filter(item => item.hash !== link.hash)
})
.catch(error => { this.showError(error) })
},
humanTime (time) {
return moment(time).fromNow()
},
buildLink (hash) {
return `${window.location.origin}${this.baseURL}/share/${hash}`
},
sort () {
this.links = this.links.sort((a, b) => {
if (!a.expires) return -1
if (!b.expires) return 1
return new Date(a.expireDate) - new Date(b.expireDate)
})
}
}
}
</script>

View File

@ -61,7 +61,7 @@
background: #fff;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
width: 95%;
max-width: 18em;
max-width: 20em;
}
#file-selection .action {
border-radius: 50%;

View File

@ -177,3 +177,32 @@
opacity: 1;
}
}
.prompt#share ul {
list-style: none;
padding: 0;
margin: 0;
}
.prompt#share ul li {
display: flex;
justify-content: space-between;
align-items: center;
}
.prompt#share ul li a {
color: #2196F3;
cursor: pointer;
margin-right: auto;
}
.prompt#share ul li .action i {
font-size: 1em;
}
.prompt#share ul li input,
.prompt#share ul li select {
padding: .2em;
margin-right: .5em;
border: 1px solid #dadada;
}

View File

@ -1,8 +1,10 @@
permanent: Permanent
buttons:
cancel: Cancel
close: Close
copy: Copy
copyFile: Copy file
copyToClipboard: Copy to clipboard
create: Create
delete: Delete
download: Download
@ -20,6 +22,7 @@ buttons:
save: Save
search: Search
select: Select
share: Share
publish: Publish
selectMultiple: Select multiple
schedule: Schedule
@ -27,6 +30,7 @@ buttons:
toggleSidebar: Toggle sidebar
update: Update
upload: Upload
permalink: Get Permanent Link
errors:
forbidden: You're not welcome here.
internal: Something really went wrong.
@ -183,3 +187,9 @@ languages:
en: English
pt: Portuguese
zhCN: Chinese (Simplified)
time:
unit: Time Unit
seconds: Seconds
minutes: Minutes
hours: Hours
days: Days

View File

@ -1,8 +1,10 @@
permanent: Permanente
buttons:
cancel: Cancelar
close: Fechar
copy: Copiar
copyFile: Copiar ficheiro
copyToClipboard: Copiar
create: Criar
delete: Eliminar
download: Descarregar
@ -19,6 +21,7 @@ buttons:
replace: Substituir
reportIssue: Reportar Erro
save: Guardar
share: Partilhar
schedule: Agendar
search: Pesquisar
select: Selecionar
@ -27,6 +30,7 @@ buttons:
toggleSidebar: Alternar barra lateral
update: Atualizar
upload: Enviar
permalink: Obter link permanente
errors:
forbidden: Tu não és bem-vindo aqui.
internal: Algo correu bastante mal.
@ -186,3 +190,9 @@ sidebar:
servedWith: Servido com
settings: Configurações
siteSettings: Configurações do Site
time:
unit: Unidades de Tempo
seconds: Segundos
minutes: Minutos
hours: Horas
days: Dias

View File

@ -1,4 +1,5 @@
import i18n from '@/i18n'
import moment from 'moment'
const mutations = {
closeHovers: state => {
@ -26,6 +27,7 @@ const mutations = {
setLoading: (state, value) => { state.loading = value },
setReload: (state, value) => { state.reload = value },
setUser: (state, value) => {
moment.locale(value.locale)
i18n.locale = value.locale
state.user = value
},

View File

@ -35,7 +35,7 @@ export function fetch (url) {
})
}
export function rm (url) {
export function remove (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
@ -383,25 +383,69 @@ export function deleteUser (id) {
})
}
export default {
removePrefix,
delete: rm,
fetch,
checksum,
move,
put,
copy,
post,
command,
search,
download,
// other things
getSettings,
updateSettings,
// User things
newUser,
getUser,
getUsers,
updateUser,
deleteUser
// SHARE
export function getShare (url) {
url = removePrefix(url)
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve(JSON.parse(request.responseText))
} else {
reject(request.status)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
export function deleteShare (hash) {
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('DELETE', `${store.state.baseURL}/api/share/${hash}`, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve()
} else {
reject(request.status)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}
export function share (url, expires = '', unit = 'hours') {
url = removePrefix(url)
url = `${store.state.baseURL}/api/share${url}`
if (expires !== '') {
url += `?expires=${expires}&unit=${unit}`
}
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', url, true)
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
request.onload = () => {
if (request.status === 200) {
resolve(JSON.parse(request.responseText))
} else {
reject(request.responseStatus)
}
}
request.onerror = (error) => reject(error)
request.send()
})
}

View File

@ -16,7 +16,7 @@ function loading (button) {
}, 100)
}
function done (button, success = true) {
function done (button) {
let el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
@ -33,7 +33,34 @@ function done (button, success = true) {
}, 100)
}
function success (button) {
let el = document.querySelector(`#${button}-button > i`)
if (el === undefined || el === null) {
console.log('Error getting button ' + button)
return
}
el.style.opacity = 0
setTimeout(() => {
el.classList.remove('spin')
el.innerHTML = 'done'
el.style.opacity = 1
setTimeout(() => {
el.style.opacity = 0
setTimeout(() => {
el.innerHTML = el.dataset.icon
el.style.opacity = 1
}, 100)
}, 500)
}, 100)
}
export default {
loading,
done
done,
success
}

View File

@ -33,7 +33,7 @@ import InternalError from './errors/500'
import Preview from '@/components/files/Preview'
import Listing from '@/components/files/Listing'
import Editor from '@/components/files/Editor'
import api from '@/utils/api'
import * as api from '@/utils/api'
import { mapGetters, mapState, mapMutations } from 'vuex'
export default {

View File

@ -31,7 +31,7 @@
</template>
<script>
import api from '@/utils/api'
import * as api from '@/utils/api'
export default {
name: 'users',

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>File Manager</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
<meta name="theme-color" content="#2979ff">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
<style>
* {
box-sizing: border-box
}
body {
font-family: Arial, sans-serif;
color: #6f6f6f;
background: #f8f8f8;
}
body > div {
text-align: center;
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
display: block;
border-radius: 0.2em;
padding: 2em 3em;
}
body > a * {
margin: 0;
}
</style>
</head>
<body>
<div><h1>404 Not Found</h1></div>
</body>
</html>

View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>{{ .File.Name }}</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
<meta name="theme-color" content="#2979ff">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
<style>
* {
box-sizing: border-box
}
body {
font-family: Arial, sans-serif;
color: #6f6f6f;
background: #f8f8f8;
}
a {
text-decoration: none;
color: inherit;
}
body > a {
text-align: center;
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
display: block;
border-radius: 0.2em;
width: 90%;
max-width: 25em;
}
body > a > div:first-child {
width: 100%;
padding: 1em;
cursor: pointer;
background: #ffffff;
color: rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
body > a > div:last-child {
padding: 2em 3em;
}
body > a * {
margin: 0;
}
body > a h1 {
margin-top: .2em;
}
</style>
</head>
<body>
<a href="?dl=1">
<div>Download {{ if .File.IsDir }}Folder{{ else }}File{{ end }}</div>
<div>
{{ if .File.IsDir -}}
<svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
{{ else -}}
<svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
{{ end -}}
<h1>{{ .File.Name }}</h1>
</div>
</a>
</body>
</html>

View File

@ -53,7 +53,7 @@ func downloadHandler(c *RequestContext, w http.ResponseWriter, r *http.Request)
}
// If the format is true, just set it to "zip".
if query == "true" {
if query == "true" || query == "" {
query = "zip"
}

View File

@ -62,11 +62,13 @@ import (
"reflect"
"regexp"
"strings"
"time"
rice "github.com/GeertJohan/go.rice"
"github.com/asdine/storm"
"github.com/hacdias/fileutils"
"github.com/mholt/caddy"
"github.com/robfig/cron"
)
var (
@ -92,6 +94,9 @@ type FileManager struct {
// The static assets.
assets *rice.Box
// Job cron.
cron *cron.Cron
// 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
// such as in caddy-filemanager plugin. It is only useful in certain situations.
@ -205,6 +210,7 @@ func New(database string, base User) (*FileManager, error) {
// map and Assets box.
m := &FileManager{
Users: map[string]*User{},
cron: cron.New(),
assets: rice.MustFindBox("./assets/dist"),
}
@ -297,6 +303,10 @@ func New(database string, base User) (*FileManager, error) {
base.Username = ""
base.Password = ""
m.DefaultUser = &base
m.cron.AddFunc("@hourly", m.shareCleaner)
m.cron.Start()
return m, nil
}
@ -406,6 +416,29 @@ func (m *FileManager) enableJekyll(j *Jekyll) error {
return nil
}
// shareCleaner removes sharing links that are no longer active.
// This function is set to run periodically.
func (m FileManager) shareCleaner() {
var links []shareLink
// Get all links.
err := m.db.All(&links)
if err != nil {
log.Print(err)
return
}
// Find the expired ones.
for i := range links {
if links[i].Expires && links[i].ExpireDate.Before(time.Now()) {
err = m.db.DeleteStruct(&links[i])
if err != nil {
log.Print(err)
}
}
}
}
// Allowed checks if the user has permission to access a directory/file.
func (u User) Allowed(url string) bool {
var rule *Rule

81
http.go
View File

@ -6,6 +6,9 @@ import (
"net/http"
"os"
"strings"
"time"
"github.com/asdine/storm"
)
// RequestContext contains the needed information to make handlers work.
@ -33,10 +36,9 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
// pass it through a template to add the needed variables.
if r.URL.Path == "/sw.js" {
return renderFile(
w,
c, w,
c.assets.MustString("sw.js"),
"application/javascript",
c,
)
}
@ -65,16 +67,20 @@ func serveHTTP(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
return c.StaticGen.Preview(c, w, r)
}
if strings.HasPrefix(r.URL.Path, "/share/") && c.StaticGen != nil {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
return sharePage(c, w, r)
}
// Any other request should show the index.html file.
w.Header().Set("x-frame-options", "SAMEORIGIN")
w.Header().Set("x-content-type", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block")
return renderFile(
w,
c, w,
c.assets.MustString("index.html"),
"text/html",
c,
)
}
@ -86,10 +92,9 @@ func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
}
return renderFile(
w,
c, w,
c.assets.MustString("static/manifest.json"),
"application/json",
c,
)
}
@ -154,6 +159,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int,
code, err = usersHandler(c, w, r)
case "settings":
code, err = settingsHandler(c, w, r)
case "share":
code, err = shareHandler(c, w, r)
default:
code = http.StatusNotFound
}
@ -194,7 +201,7 @@ func splitURL(path string) (string, string) {
}
// renderFile renders a file using a template with some needed variables.
func renderFile(w http.ResponseWriter, file string, contentType string, c *RequestContext) (int, error) {
func renderFile(c *RequestContext, w http.ResponseWriter, file string, contentType string) (int, error) {
tpl := template.Must(template.New("file").Parse(file))
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
@ -209,6 +216,66 @@ func renderFile(w http.ResponseWriter, file string, contentType string, c *Reque
return 0, nil
}
func sharePage(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", r.URL.Path, &s)
if err == storm.ErrNotFound {
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
}
if err != nil {
return http.StatusInternalServerError, err
}
if s.Expires && s.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&s)
return renderFile(
c, w,
c.assets.MustString("static/share/404.html"),
"text/html",
)
}
r.URL.Path = s.Path
info, err := os.Stat(s.Path)
if err != nil {
return errorToHTTP(err, false), err
}
c.File = &file{
Path: s.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
}
dl := r.URL.Query().Get("dl")
if dl == "" || dl == "0" {
tpl := template.Must(template.New("file").Parse(c.assets.MustString("static/share/index.html")))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(),
"File": c.File,
})
if err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
return downloadHandler(c, w, r)
}
// renderJSON prints the JSON version of data to the browser.
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
marsh, err := json.Marshal(data)

View File

@ -9,6 +9,7 @@
"lint": "eslint --ext .js,.vue assets/src"
},
"dependencies": {
"clipboard": "^1.7.1",
"codemirror": "^5.27.4",
"filesize": "^3.5.10",
"moment": "^2.18.1",

View File

@ -14,7 +14,6 @@ import (
"time"
"github.com/hacdias/fileutils"
"github.com/robfig/cron"
)
// sanitizeURL sanitizes the URL to prevent path transversal
@ -174,7 +173,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
}
// Otherwise we try to create the directory.
err := c.User.FileSystem.Mkdir(r.URL.Path, 0666)
err := c.User.FileSystem.Mkdir(r.URL.Path, 0776)
return errorToHTTP(err, false), err
}
@ -188,7 +187,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re
}
// Create/Open the file.
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
if err != nil {
return errorToHTTP(err, false), err
}
@ -242,15 +241,13 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
return http.StatusInternalServerError, err
}
scheduler := cron.New()
scheduler.AddFunc(t.Format("05 04 15 02 01 *"), func() {
c.cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
_, err := resourcePublish(c, w, r)
if err != nil {
log.Print(err)
}
})
scheduler.Start()
return http.StatusOK, nil
}

File diff suppressed because one or more lines are too long

137
share.go Normal file
View File

@ -0,0 +1,137 @@
package filemanager
import (
"encoding/hex"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/asdine/storm"
"github.com/asdine/storm/q"
)
type shareLink struct {
Hash string `json:"hash" storm:"id,index"`
Path string `json:"path" storm:"index"`
Expires bool `json:"expires"`
ExpireDate time.Time `json:"expireDate"`
}
func shareHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method {
case http.MethodGet:
return shareGetHandler(c, w, r)
case http.MethodDelete:
return shareDeleteHandler(c, w, r)
case http.MethodPost:
return sharePostHandler(c, w, r)
}
return http.StatusNotImplemented, nil
}
func shareGetHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var (
s []*shareLink
path = filepath.Join(string(c.User.FileSystem), r.URL.Path)
)
err := c.db.Find("Path", path, &s)
if err == storm.ErrNotFound {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
for i, link := range s {
if link.Expires && link.ExpireDate.Before(time.Now()) {
c.db.DeleteStruct(&shareLink{Hash: link.Hash})
s = append(s[:i], s[i+1:]...)
}
}
return renderJSON(w, s)
}
func sharePostHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(string(c.User.FileSystem), r.URL.Path)
var s shareLink
expire := r.URL.Query().Get("expires")
unit := r.URL.Query().Get("unit")
if expire == "" {
err := c.db.Select(q.Eq("Path", path), q.Eq("Expires", false)).First(&s)
if err == nil {
w.Write([]byte(c.RootURL() + "/share/" + s.Hash))
return 0, nil
}
}
bytes, err := generateRandomBytes(32)
if err != nil {
return http.StatusInternalServerError, err
}
str := hex.EncodeToString(bytes)
s = shareLink{
Path: path,
Hash: str,
Expires: expire != "",
}
if expire != "" {
num, err := strconv.Atoi(expire)
if err != nil {
return http.StatusInternalServerError, err
}
var add time.Duration
switch unit {
case "seconds":
add = time.Second * time.Duration(num)
case "minutes":
add = time.Minute * time.Duration(num)
case "days":
add = time.Hour * 24 * time.Duration(num)
default:
add = time.Hour * time.Duration(num)
}
s.ExpireDate = time.Now().Add(add)
}
err = c.db.Save(&s)
if err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, s)
}
func shareDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
var s shareLink
err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s)
if err == storm.ErrNotFound {
return http.StatusNotFound, nil
}
if err != nil {
return http.StatusInternalServerError, err
}
err = c.db.DeleteStruct(&s)
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusOK, nil
}