change new folder and file permissions #190
progresses on sharing #192 Progresses on #192 Progresses on #192 Little API update Build assets Former-commit-id: 68e70132ea857eb65638c0496c030be1c181ed1c [formerly d67b74280b7f12c3e20de6abe31fcfc26e8f43ef] [formerly 8fe91e003c9616da23f0e673ad4bb89d792a41c8 [formerly 868434360592aa0280e0d631840750d53a564cd3]] Former-commit-id: 7d22ff468e580601d0c3e0921734b587b92484f8 [formerly 55f9d830636f9bbf15e0453d1ee7de6ee5d5191e] Former-commit-id: ad411a5979521dda9ea9683d86e4c8ae7b3c9e6f
This commit is contained in:
parent
25a86a9382
commit
8d715bb433
|
@ -9,7 +9,7 @@
|
||||||
<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">
|
||||||
<!--[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 -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
|
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
|
||||||
<meta name="theme-color" content="#2979ff">
|
<meta name="theme-color" content="#2979ff">
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
||||||
<div id="file-selection" v-if="isMobile && req.kind === 'listing'">
|
<div id="file-selection" v-if="isMobile && req.kind === 'listing'">
|
||||||
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
||||||
|
<share-button v-show="showRenameButton"></share-button>
|
||||||
<rename-button v-show="showRenameButton"></rename-button>
|
<rename-button v-show="showRenameButton"></rename-button>
|
||||||
<copy-button v-show="showMoveButton"></copy-button>
|
<copy-button v-show="showMoveButton"></copy-button>
|
||||||
<move-button v-show="showMoveButton"></move-button>
|
<move-button v-show="showMoveButton"></move-button>
|
||||||
|
@ -38,6 +39,7 @@
|
||||||
<!-- This buttons are shown on a dropdown on mobile phones -->
|
<!-- This buttons are shown on a dropdown on mobile phones -->
|
||||||
<div id="dropdown" :class="{ active: showMore }">
|
<div id="dropdown" :class="{ active: showMore }">
|
||||||
<div v-if="!isListing || !isMobile">
|
<div v-if="!isListing || !isMobile">
|
||||||
|
<share-button v-show="showRenameButton"></share-button>
|
||||||
<rename-button v-show="showRenameButton"></rename-button>
|
<rename-button v-show="showRenameButton"></rename-button>
|
||||||
<copy-button v-show="showMoveButton"></copy-button>
|
<copy-button v-show="showMoveButton"></copy-button>
|
||||||
<move-button v-show="showMoveButton"></move-button>
|
<move-button v-show="showMoveButton"></move-button>
|
||||||
|
@ -74,6 +76,7 @@ import SwitchButton from './buttons/SwitchView'
|
||||||
import MoveButton from './buttons/Move'
|
import MoveButton from './buttons/Move'
|
||||||
import CopyButton from './buttons/Copy'
|
import CopyButton from './buttons/Copy'
|
||||||
import ScheduleButton from './buttons/Schedule'
|
import ScheduleButton from './buttons/Schedule'
|
||||||
|
import ShareButton from './buttons/Share'
|
||||||
import {mapGetters, mapState} from 'vuex'
|
import {mapGetters, mapState} from 'vuex'
|
||||||
import * as api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
@ -84,6 +87,7 @@ export default {
|
||||||
Search,
|
Search,
|
||||||
InfoButton,
|
InfoButton,
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
|
ShareButton,
|
||||||
RenameButton,
|
RenameButton,
|
||||||
DownloadButton,
|
DownloadButton,
|
||||||
CopyButton,
|
CopyButton,
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'search',
|
name: 'search',
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapGetters, mapState} from 'vuex'
|
import {mapGetters, mapState} from 'vuex'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'download-button',
|
name: 'download-button',
|
||||||
|
|
|
@ -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>
|
|
@ -11,7 +11,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import CodeMirror from '@/utils/codemirror'
|
import CodeMirror from '@/utils/codemirror'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -129,7 +129,7 @@ export default {
|
||||||
|
|
||||||
api.put(this.$route.path, content, regenerate, this.schedule)
|
api.put(this.$route.path, content, regenerate, this.schedule)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done(button)
|
buttons.success(button)
|
||||||
this.$store.commit('setSchedule', '')
|
this.$store.commit('setSchedule', '')
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
import Item from './ListingItem'
|
import Item from './ListingItem'
|
||||||
import css from '@/utils/css'
|
import css from '@/utils/css'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -325,7 +325,7 @@ export default {
|
||||||
|
|
||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('upload')
|
buttons.success('upload')
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||||
import filesize from 'filesize'
|
import filesize from 'filesize'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'item',
|
name: 'item',
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import InfoButton from '@/components/buttons/Info'
|
import InfoButton from '@/components/buttons/Info'
|
||||||
import DeleteButton from '@/components/buttons/Delete'
|
import DeleteButton from '@/components/buttons/Delete'
|
||||||
import RenameButton from '@/components/buttons/Rename'
|
import RenameButton from '@/components/buttons/Rename'
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -51,7 +51,7 @@ export default {
|
||||||
// Execute the promises.
|
// Execute the promises.
|
||||||
api.copy(items)
|
api.copy(items)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('copy')
|
buttons.success('copy')
|
||||||
this.$router.push({ path: this.dest })
|
this.$router.push({ path: this.dest })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapGetters, mapMutations, mapState} from 'vuex'
|
import {mapGetters, mapMutations, mapState} from 'vuex'
|
||||||
import api from '@/utils/api'
|
import { remove } from '@/utils/api'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
|
@ -36,9 +36,9 @@ export default {
|
||||||
// If we are not on a listing, delete the current
|
// If we are not on a listing, delete the current
|
||||||
// opened file.
|
// opened file.
|
||||||
if (this.req.kind !== 'listing') {
|
if (this.req.kind !== 'listing') {
|
||||||
api.delete(this.$route.path)
|
remove(this.$route.path)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('delete')
|
buttons.success('delete')
|
||||||
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
|
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -59,12 +59,12 @@ export default {
|
||||||
let promises = []
|
let promises = []
|
||||||
|
|
||||||
for (let index of this.selected) {
|
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)
|
Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('delete')
|
buttons.success('delete')
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapGetters, mapState} from 'vuex'
|
import {mapGetters, mapState} from 'vuex'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'download',
|
name: 'download',
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'file-list',
|
name: 'file-list',
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
import {mapState, mapGetters} from 'vuex'
|
import {mapState, mapGetters} from 'vuex'
|
||||||
import filesize from 'filesize'
|
import filesize from 'filesize'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'info',
|
name: 'info',
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -51,7 +51,7 @@ export default {
|
||||||
// Execute the promises.
|
// Execute the promises.
|
||||||
api.move(items)
|
api.move(items)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('move')
|
buttons.success('move')
|
||||||
this.$router.push({ path: this.dest })
|
this.$router.push({ path: this.dest })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'new-dir',
|
name: 'new-dir',
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'new-file',
|
name: 'new-file',
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<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>
|
||||||
|
<share v-else-if="show === 'share'"></share>
|
||||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -33,9 +34,10 @@ import NewDir from './NewDir'
|
||||||
import NewArchetype from './NewArchetype'
|
import NewArchetype from './NewArchetype'
|
||||||
import Replace from './Replace'
|
import Replace from './Replace'
|
||||||
import Schedule from './Schedule'
|
import Schedule from './Schedule'
|
||||||
|
import Share from './Share'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'prompts',
|
name: 'prompts',
|
||||||
|
@ -50,6 +52,7 @@ export default {
|
||||||
Success,
|
Success,
|
||||||
Move,
|
Move,
|
||||||
Copy,
|
Copy,
|
||||||
|
Share,
|
||||||
NewFile,
|
NewFile,
|
||||||
NewDir,
|
NewDir,
|
||||||
Help,
|
Help,
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import url from '@/utils/url'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'rename',
|
name: 'rename',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
max-width: 18em;
|
max-width: 20em;
|
||||||
}
|
}
|
||||||
#file-selection .action {
|
#file-selection .action {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
|
@ -177,3 +177,32 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prompt#share ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt#share ul li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt#share ul li a {
|
||||||
|
color: #2196F3;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt#share ul li .action i {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt#share ul li input,
|
||||||
|
.prompt#share ul li select {
|
||||||
|
padding: .2em;
|
||||||
|
margin-right: .5em;
|
||||||
|
border: 1px solid #dadada;
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
permanent: Permanent
|
||||||
buttons:
|
buttons:
|
||||||
cancel: Cancel
|
cancel: Cancel
|
||||||
close: Close
|
close: Close
|
||||||
copy: Copy
|
copy: Copy
|
||||||
copyFile: Copy file
|
copyFile: Copy file
|
||||||
|
copyToClipboard: Copy to clipboard
|
||||||
create: Create
|
create: Create
|
||||||
delete: Delete
|
delete: Delete
|
||||||
download: Download
|
download: Download
|
||||||
|
@ -20,6 +22,7 @@ buttons:
|
||||||
save: Save
|
save: Save
|
||||||
search: Search
|
search: Search
|
||||||
select: Select
|
select: Select
|
||||||
|
share: Share
|
||||||
publish: Publish
|
publish: Publish
|
||||||
selectMultiple: Select multiple
|
selectMultiple: Select multiple
|
||||||
schedule: Schedule
|
schedule: Schedule
|
||||||
|
@ -27,6 +30,7 @@ buttons:
|
||||||
toggleSidebar: Toggle sidebar
|
toggleSidebar: Toggle sidebar
|
||||||
update: Update
|
update: Update
|
||||||
upload: Upload
|
upload: Upload
|
||||||
|
permalink: Get Permanent Link
|
||||||
errors:
|
errors:
|
||||||
forbidden: You're not welcome here.
|
forbidden: You're not welcome here.
|
||||||
internal: Something really went wrong.
|
internal: Something really went wrong.
|
||||||
|
@ -183,3 +187,9 @@ languages:
|
||||||
en: English
|
en: English
|
||||||
pt: Portuguese
|
pt: Portuguese
|
||||||
zhCN: Chinese (Simplified)
|
zhCN: Chinese (Simplified)
|
||||||
|
time:
|
||||||
|
unit: Time Unit
|
||||||
|
seconds: Seconds
|
||||||
|
minutes: Minutes
|
||||||
|
hours: Hours
|
||||||
|
days: Days
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
permanent: Permanente
|
||||||
buttons:
|
buttons:
|
||||||
cancel: Cancelar
|
cancel: Cancelar
|
||||||
close: Fechar
|
close: Fechar
|
||||||
copy: Copiar
|
copy: Copiar
|
||||||
copyFile: Copiar ficheiro
|
copyFile: Copiar ficheiro
|
||||||
|
copyToClipboard: Copiar
|
||||||
create: Criar
|
create: Criar
|
||||||
delete: Eliminar
|
delete: Eliminar
|
||||||
download: Descarregar
|
download: Descarregar
|
||||||
|
@ -19,6 +21,7 @@ buttons:
|
||||||
replace: Substituir
|
replace: Substituir
|
||||||
reportIssue: Reportar Erro
|
reportIssue: Reportar Erro
|
||||||
save: Guardar
|
save: Guardar
|
||||||
|
share: Partilhar
|
||||||
schedule: Agendar
|
schedule: Agendar
|
||||||
search: Pesquisar
|
search: Pesquisar
|
||||||
select: Selecionar
|
select: Selecionar
|
||||||
|
@ -27,6 +30,7 @@ buttons:
|
||||||
toggleSidebar: Alternar barra lateral
|
toggleSidebar: Alternar barra lateral
|
||||||
update: Atualizar
|
update: Atualizar
|
||||||
upload: Enviar
|
upload: Enviar
|
||||||
|
permalink: Obter link permanente
|
||||||
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.
|
||||||
|
@ -186,3 +190,9 @@ sidebar:
|
||||||
servedWith: Servido com
|
servedWith: Servido com
|
||||||
settings: Configurações
|
settings: Configurações
|
||||||
siteSettings: Configurações do Site
|
siteSettings: Configurações do Site
|
||||||
|
time:
|
||||||
|
unit: Unidades de Tempo
|
||||||
|
seconds: Segundos
|
||||||
|
minutes: Minutos
|
||||||
|
hours: Horas
|
||||||
|
days: Dias
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
closeHovers: state => {
|
closeHovers: state => {
|
||||||
|
@ -26,6 +27,7 @@ const mutations = {
|
||||||
setLoading: (state, value) => { state.loading = value },
|
setLoading: (state, value) => { state.loading = value },
|
||||||
setReload: (state, value) => { state.reload = value },
|
setReload: (state, value) => { state.reload = value },
|
||||||
setUser: (state, value) => {
|
setUser: (state, value) => {
|
||||||
|
moment.locale(value.locale)
|
||||||
i18n.locale = value.locale
|
i18n.locale = value.locale
|
||||||
state.user = value
|
state.user = value
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,7 +35,7 @@ export function fetch (url) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rm (url) {
|
export function remove (url) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -383,25 +383,69 @@ export function deleteUser (id) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
// SHARE
|
||||||
removePrefix,
|
|
||||||
delete: rm,
|
export function getShare (url) {
|
||||||
fetch,
|
url = removePrefix(url)
|
||||||
checksum,
|
|
||||||
move,
|
return new Promise((resolve, reject) => {
|
||||||
put,
|
let request = new window.XMLHttpRequest()
|
||||||
copy,
|
request.open('GET', `${store.state.baseURL}/api/share${url}`, true)
|
||||||
post,
|
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
command,
|
|
||||||
search,
|
request.onload = () => {
|
||||||
download,
|
if (request.status === 200) {
|
||||||
// other things
|
resolve(JSON.parse(request.responseText))
|
||||||
getSettings,
|
} else {
|
||||||
updateSettings,
|
reject(request.status)
|
||||||
// User things
|
}
|
||||||
newUser,
|
}
|
||||||
getUser,
|
|
||||||
getUsers,
|
request.onerror = (error) => reject(error)
|
||||||
updateUser,
|
request.send()
|
||||||
deleteUser
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ function loading (button) {
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
function done (button, success = true) {
|
function done (button) {
|
||||||
let el = document.querySelector(`#${button}-button > i`)
|
let el = document.querySelector(`#${button}-button > i`)
|
||||||
|
|
||||||
if (el === undefined || el === null) {
|
if (el === undefined || el === null) {
|
||||||
|
@ -33,7 +33,34 @@ function done (button, success = true) {
|
||||||
}, 100)
|
}, 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 {
|
export default {
|
||||||
loading,
|
loading,
|
||||||
done
|
done,
|
||||||
|
success
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ import InternalError from './errors/500'
|
||||||
import Preview from '@/components/files/Preview'
|
import Preview from '@/components/files/Preview'
|
||||||
import Listing from '@/components/files/Listing'
|
import Listing from '@/components/files/Listing'
|
||||||
import Editor from '@/components/files/Editor'
|
import Editor from '@/components/files/Editor'
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
import { mapGetters, mapState, mapMutations } from 'vuex'
|
import { mapGetters, mapState, mapMutations } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import api from '@/utils/api'
|
import * as api from '@/utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'users',
|
name: 'users',
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 the format is true, just set it to "zip".
|
||||||
if query == "true" {
|
if query == "true" || query == "" {
|
||||||
query = "zip"
|
query = "zip"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,11 +62,13 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
rice "github.com/GeertJohan/go.rice"
|
rice "github.com/GeertJohan/go.rice"
|
||||||
"github.com/asdine/storm"
|
"github.com/asdine/storm"
|
||||||
"github.com/hacdias/fileutils"
|
"github.com/hacdias/fileutils"
|
||||||
"github.com/mholt/caddy"
|
"github.com/mholt/caddy"
|
||||||
|
"github.com/robfig/cron"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -92,6 +94,9 @@ type FileManager struct {
|
||||||
// The static assets.
|
// The static assets.
|
||||||
assets *rice.Box
|
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
|
// 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
|
||||||
// such as in caddy-filemanager plugin. It is only useful in certain situations.
|
// 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.
|
// map and Assets box.
|
||||||
m := &FileManager{
|
m := &FileManager{
|
||||||
Users: map[string]*User{},
|
Users: map[string]*User{},
|
||||||
|
cron: cron.New(),
|
||||||
assets: rice.MustFindBox("./assets/dist"),
|
assets: rice.MustFindBox("./assets/dist"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,6 +303,10 @@ func New(database string, base User) (*FileManager, error) {
|
||||||
base.Username = ""
|
base.Username = ""
|
||||||
base.Password = ""
|
base.Password = ""
|
||||||
m.DefaultUser = &base
|
m.DefaultUser = &base
|
||||||
|
|
||||||
|
m.cron.AddFunc("@hourly", m.shareCleaner)
|
||||||
|
m.cron.Start()
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,6 +416,29 @@ func (m *FileManager) enableJekyll(j *Jekyll) error {
|
||||||
return nil
|
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.
|
// Allowed checks if the user has permission to access a directory/file.
|
||||||
func (u User) Allowed(url string) bool {
|
func (u User) Allowed(url string) bool {
|
||||||
var rule *Rule
|
var rule *Rule
|
||||||
|
|
81
http.go
81
http.go
|
@ -6,6 +6,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/asdine/storm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequestContext contains the needed information to make handlers work.
|
// 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.
|
// 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(
|
||||||
w,
|
c, w,
|
||||||
c.assets.MustString("sw.js"),
|
c.assets.MustString("sw.js"),
|
||||||
"application/javascript",
|
"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)
|
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.
|
// Any other request should show the index.html file.
|
||||||
w.Header().Set("x-frame-options", "SAMEORIGIN")
|
w.Header().Set("x-frame-options", "SAMEORIGIN")
|
||||||
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(
|
||||||
w,
|
c, w,
|
||||||
c.assets.MustString("index.html"),
|
c.assets.MustString("index.html"),
|
||||||
"text/html",
|
"text/html",
|
||||||
c,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,10 +92,9 @@ func staticHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (i
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderFile(
|
return renderFile(
|
||||||
w,
|
c, w,
|
||||||
c.assets.MustString("static/manifest.json"),
|
c.assets.MustString("static/manifest.json"),
|
||||||
"application/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)
|
code, err = usersHandler(c, w, r)
|
||||||
case "settings":
|
case "settings":
|
||||||
code, err = settingsHandler(c, w, r)
|
code, err = settingsHandler(c, w, r)
|
||||||
|
case "share":
|
||||||
|
code, err = shareHandler(c, w, r)
|
||||||
default:
|
default:
|
||||||
code = http.StatusNotFound
|
code = http.StatusNotFound
|
||||||
}
|
}
|
||||||
|
@ -194,7 +201,7 @@ 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(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))
|
tpl := template.Must(template.New("file").Parse(file))
|
||||||
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
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
|
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.
|
// renderJSON prints the JSON version of data to the browser.
|
||||||
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
|
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
|
||||||
marsh, err := json.Marshal(data)
|
marsh, err := json.Marshal(data)
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"lint": "eslint --ext .js,.vue assets/src"
|
"lint": "eslint --ext .js,.vue assets/src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clipboard": "^1.7.1",
|
||||||
"codemirror": "^5.27.4",
|
"codemirror": "^5.27.4",
|
||||||
"filesize": "^3.5.10",
|
"filesize": "^3.5.10",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
|
|
|
@ -14,7 +14,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hacdias/fileutils"
|
"github.com/hacdias/fileutils"
|
||||||
"github.com/robfig/cron"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// sanitizeURL sanitizes the URL to prevent path transversal
|
// 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.
|
// 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
|
return errorToHTTP(err, false), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +187,7 @@ 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, 0666)
|
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
|
||||||
}
|
}
|
||||||
|
@ -242,15 +241,13 @@ func resourcePublishSchedule(c *RequestContext, w http.ResponseWriter, r *http.R
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduler := cron.New()
|
c.cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
|
||||||
scheduler.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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
scheduler.Start()
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
d12113f698d3e44cb873181f6e7d7c9e17de30ac
|
61f496e0973436714c2e195318148550e39bbe26
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue