Copy and cut files
Former-commit-id: ac9399efef37f13706590384e29295c6234acb7c [formerly f62dcb89aa49125769405c502688f3f8d4348da3] [formerly 4b9f0fd696f449c30da2d2f7a11cf5d6c7054c54 [formerly 64e70e39dcf6759e04315bc97ec6a0fed927c737]] Former-commit-id: 422165231c785b121b1f6725909d3ce7c8dcdfee [formerly 2b26582a03f674d40abb39e27b2cc880d2bd3ebb] Former-commit-id: ee743808a3c012514c9b7d57a34c79e408a8a78b
This commit is contained in:
parent
42bab1458e
commit
149465ab52
|
@ -38,6 +38,7 @@
|
||||||
<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>
|
||||||
<rename-button v-show="showRenameButton"></rename-button>
|
<rename-button v-show="showRenameButton"></rename-button>
|
||||||
|
<copy-button v-show="showMoveButton"></copy-button>
|
||||||
<move-button v-show="showMoveButton"></move-button>
|
<move-button v-show="showMoveButton"></move-button>
|
||||||
<delete-button v-show="showDeleteButton"></delete-button>
|
<delete-button v-show="showDeleteButton"></delete-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
<div id="dropdown" :class="{ active: showMore }">
|
<div id="dropdown" :class="{ active: showMore }">
|
||||||
<div v-if="!isListing || !isMobile">
|
<div v-if="!isListing || !isMobile">
|
||||||
<rename-button v-show="showRenameButton"></rename-button>
|
<rename-button v-show="showRenameButton"></rename-button>
|
||||||
|
<copy-button v-show="showMoveButton"></copy-button>
|
||||||
<move-button v-show="showMoveButton"></move-button>
|
<move-button v-show="showMoveButton"></move-button>
|
||||||
<delete-button v-show="showDeleteButton"></delete-button>
|
<delete-button v-show="showDeleteButton"></delete-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -88,6 +90,7 @@ import UploadButton from './buttons/Upload'
|
||||||
import DownloadButton from './buttons/Download'
|
import DownloadButton from './buttons/Download'
|
||||||
import SwitchButton from './buttons/SwitchView'
|
import SwitchButton from './buttons/SwitchView'
|
||||||
import MoveButton from './buttons/Move'
|
import MoveButton from './buttons/Move'
|
||||||
|
import CopyButton from './buttons/Copy'
|
||||||
import {mapGetters, mapState} from 'vuex'
|
import {mapGetters, mapState} from 'vuex'
|
||||||
import api from '@/utils/api'
|
import api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
@ -100,6 +103,7 @@ export default {
|
||||||
DeleteButton,
|
DeleteButton,
|
||||||
RenameButton,
|
RenameButton,
|
||||||
DownloadButton,
|
DownloadButton,
|
||||||
|
CopyButton,
|
||||||
UploadButton,
|
UploadButton,
|
||||||
SwitchButton,
|
SwitchButton,
|
||||||
MoveButton
|
MoveButton
|
||||||
|
|
|
@ -82,7 +82,7 @@ export default {
|
||||||
name: 'listing',
|
name: 'listing',
|
||||||
components: { Item },
|
components: { Item },
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['req']),
|
...mapState(['req', 'selected']),
|
||||||
nameSorted () {
|
nameSorted () {
|
||||||
return (this.req.sort === 'name')
|
return (this.req.sort === 'name')
|
||||||
},
|
},
|
||||||
|
@ -130,17 +130,69 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (String.fromCharCode(event.which).toLowerCase() !== 'f') {
|
let key = String.fromCharCode(event.which).toLowerCase()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault()
|
switch (key) {
|
||||||
this.$store.commit('showHover', 'search')
|
case 'f':
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('showHover', 'search')
|
||||||
|
break
|
||||||
|
case 'c':
|
||||||
|
case 'x':
|
||||||
|
this.copyCut(event, key)
|
||||||
|
break
|
||||||
|
case 'v':
|
||||||
|
this.paste(event)
|
||||||
|
break
|
||||||
|
}
|
||||||
},
|
},
|
||||||
preventDefault (event) {
|
preventDefault (event) {
|
||||||
// Wrapper around prevent default.
|
// Wrapper around prevent default.
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
},
|
},
|
||||||
|
copyCut (event, key) {
|
||||||
|
event.preventDefault()
|
||||||
|
let items = []
|
||||||
|
|
||||||
|
for (let i of this.selected) {
|
||||||
|
items.push({
|
||||||
|
from: this.req.items[i].url,
|
||||||
|
name: encodeURIComponent(this.req.items[i].name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.commit('updateClipboard', {
|
||||||
|
key: key,
|
||||||
|
items: items
|
||||||
|
})
|
||||||
|
},
|
||||||
|
paste (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
let items = []
|
||||||
|
|
||||||
|
for (let item of this.$store.state.clipboard.items) {
|
||||||
|
items.push({
|
||||||
|
from: item.from,
|
||||||
|
to: this.$route.path + item.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.state.clipboard.key === 'x') {
|
||||||
|
api.move(items).then(() => {
|
||||||
|
this.$store.commit('setReload', true)
|
||||||
|
}).catch(error => {
|
||||||
|
this.$store.commit('showError', error)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.copy(items).then(() => {
|
||||||
|
this.$store.commit('setReload', true)
|
||||||
|
}).catch(error => {
|
||||||
|
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.
|
||||||
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
|
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
|
||||||
|
|
|
@ -92,16 +92,16 @@ export default {
|
||||||
|
|
||||||
if (this.selectedCount === 0) return
|
if (this.selectedCount === 0) return
|
||||||
|
|
||||||
let promises = []
|
let items = []
|
||||||
|
|
||||||
for (let i of this.selected) {
|
for (let i of this.selected) {
|
||||||
let url = this.req.items[i].url
|
items.push({
|
||||||
let name = this.req.items[i].name
|
from: this.req.items[i].url,
|
||||||
|
to: this.url + encodeURIComponent(this.req.items[i].name)
|
||||||
promises.push(api.move(url, this.url + encodeURIComponent(name)))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(promises)
|
api.move(items)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<button @click="show" aria-label="Copy" title="Copy" class="action" id="copy-button">
|
||||||
|
<i class="material-icons">content_copy</i>
|
||||||
|
<span>Copy file</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'copy-button',
|
||||||
|
methods: {
|
||||||
|
show: function (event) {
|
||||||
|
this.$store.commit('showHover', 'copy')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<div class="prompt">
|
||||||
|
<h3>Copy</h3>
|
||||||
|
<p>Choose the place to copy your files:</p>
|
||||||
|
|
||||||
|
<file-list @update:selected="val => dest = val"></file-list>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="ok" @click="copy">Copy</button>
|
||||||
|
<button class="cancel" @click="$store.commit('closeHovers')">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import FileList from './FileList'
|
||||||
|
import api from '@/utils/api'
|
||||||
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'copy',
|
||||||
|
components: { FileList },
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
current: window.location.pathname,
|
||||||
|
dest: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: mapState(['req', 'selected']),
|
||||||
|
methods: {
|
||||||
|
copy: function (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
buttons.loading('copy')
|
||||||
|
let items = []
|
||||||
|
|
||||||
|
// Create a new promise for each file.
|
||||||
|
for (let item of this.selected) {
|
||||||
|
items.push({
|
||||||
|
from: this.req.items[item].url,
|
||||||
|
to: this.dest + encodeURIComponent(this.req.items[item].name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the promises.
|
||||||
|
api.copy(items)
|
||||||
|
.then(() => {
|
||||||
|
buttons.done('copy')
|
||||||
|
this.$router.push({ path: this.dest })
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
buttons.done('copy')
|
||||||
|
this.$store.commit('showError', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,137 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ul class="file-list">
|
||||||
|
<li @click="select"
|
||||||
|
@touchstart="touchstart"
|
||||||
|
@dblclick="next"
|
||||||
|
:aria-selected="selected == item.url"
|
||||||
|
:key="item.name" v-for="item in items"
|
||||||
|
:data-url="item.url">{{ item.name }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Currently navigating on: <code>{{ nav }}</code>.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import url from '@/utils/url'
|
||||||
|
import api from '@/utils/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'file-list',
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
touches: {
|
||||||
|
id: '',
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
selected: null,
|
||||||
|
current: window.location.pathname
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['req']),
|
||||||
|
nav () {
|
||||||
|
return decodeURIComponent(this.current)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
// If we're showing this on a listing,
|
||||||
|
// we can use the current request object
|
||||||
|
// to fill the move options.
|
||||||
|
if (this.req.kind === 'listing') {
|
||||||
|
this.fillOptions(this.req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we must be on a preview or editor
|
||||||
|
// so we fetch the data from the previous directory.
|
||||||
|
api.fetch(url.removeLastDir(this.$route.path))
|
||||||
|
.then(this.fillOptions)
|
||||||
|
.catch(this.showError)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fillOptions (req) {
|
||||||
|
// Sets the current path and resets
|
||||||
|
// the current items.
|
||||||
|
this.current = req.url
|
||||||
|
this.items = []
|
||||||
|
|
||||||
|
this.$emit('update:selected', this.current)
|
||||||
|
|
||||||
|
// If the path isn't the root path,
|
||||||
|
// show a button to navigate to the previous
|
||||||
|
// directory.
|
||||||
|
if (req.url !== '/files/') {
|
||||||
|
this.items.push({
|
||||||
|
name: '..',
|
||||||
|
url: url.removeLastDir(req.url) + '/'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this folder is empty, finish here.
|
||||||
|
if (req.items === null) return
|
||||||
|
|
||||||
|
// Otherwise we add every directory to the
|
||||||
|
// move options.
|
||||||
|
for (let item of req.items) {
|
||||||
|
if (!item.isDir) continue
|
||||||
|
|
||||||
|
this.items.push({
|
||||||
|
name: item.name,
|
||||||
|
url: item.url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
next: function (event) {
|
||||||
|
// Retrieves the URL of the directory the user
|
||||||
|
// just clicked in and fill the options with its
|
||||||
|
// content.
|
||||||
|
let uri = event.currentTarget.dataset.url
|
||||||
|
|
||||||
|
api.fetch(uri)
|
||||||
|
.then(this.fillOptions)
|
||||||
|
.catch(this.showError)
|
||||||
|
},
|
||||||
|
touchstart (event) {
|
||||||
|
let url = event.currentTarget.dataset.url
|
||||||
|
|
||||||
|
// In 300 milliseconds, we shall reset the count.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.touches.count = 0
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
// If the element the user is touching
|
||||||
|
// is different from the last one he touched,
|
||||||
|
// reset the count.
|
||||||
|
if (this.touches.id !== url) {
|
||||||
|
this.touches.id = url
|
||||||
|
this.touches.count = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.touches.count++
|
||||||
|
|
||||||
|
// If there is more than one touch already,
|
||||||
|
// open the next screen.
|
||||||
|
if (this.touches.count > 1) {
|
||||||
|
this.next(event)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: function (event) {
|
||||||
|
// If the element is already selected, unselect it.
|
||||||
|
if (this.selected === event.currentTarget.dataset.url) {
|
||||||
|
this.selected = null
|
||||||
|
this.$emit('update:selected', this.current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise select the element.
|
||||||
|
this.selected = event.currentTarget.dataset.url
|
||||||
|
this.$emit('update:selected', this.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -3,16 +3,7 @@
|
||||||
<h3>Move</h3>
|
<h3>Move</h3>
|
||||||
<p>Choose new house for your file(s)/folder(s):</p>
|
<p>Choose new house for your file(s)/folder(s):</p>
|
||||||
|
|
||||||
<ul class="file-list">
|
<file-list @update:selected="val => dest = val"></file-list>
|
||||||
<li @click="select"
|
|
||||||
@touchstart="touchstart"
|
|
||||||
@dblclick="next"
|
|
||||||
:aria-selected="moveTo == item.url"
|
|
||||||
:key="item.name" v-for="item in items"
|
|
||||||
:data-url="item.url">{{ item.name }}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Currently navigating on: <code>{{ current }}</code>.</p>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="ok" @click="move">Move</button>
|
<button class="ok" @click="move">Move</button>
|
||||||
|
@ -23,145 +14,46 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import url from '@/utils/url'
|
import FileList from './FileList'
|
||||||
import api from '@/utils/api'
|
import api from '@/utils/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'move',
|
name: 'move',
|
||||||
|
components: { FileList },
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
items: [],
|
|
||||||
touches: {
|
|
||||||
id: '',
|
|
||||||
count: 0
|
|
||||||
},
|
|
||||||
current: window.location.pathname,
|
current: window.location.pathname,
|
||||||
moveTo: null
|
dest: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: mapState(['req', 'selected', 'baseURL']),
|
computed: mapState(['req', 'selected']),
|
||||||
mounted () {
|
|
||||||
// If we're showing this on a listing,
|
|
||||||
// we can use the current request object
|
|
||||||
// to fill the move options.
|
|
||||||
if (this.req.kind === 'listing') {
|
|
||||||
this.fillOptions(this.req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, we must be on a preview or editor
|
|
||||||
// so we fetch the data from the previous directory.
|
|
||||||
api.fetch(url.removeLastDir(this.$rute.path))
|
|
||||||
.then(this.fillOptions)
|
|
||||||
.catch(this.showError)
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
move: function (event) {
|
move: function (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
// Set the destination and create the promises array.
|
|
||||||
let promises = []
|
|
||||||
let dest = (this.moveTo === null) ? this.current : this.moveTo
|
|
||||||
buttons.loading('move')
|
buttons.loading('move')
|
||||||
|
let items = []
|
||||||
|
|
||||||
// Create a new promise for each file.
|
// Create a new promise for each file.
|
||||||
for (let item of this.selected) {
|
for (let item of this.selected) {
|
||||||
let from = this.req.items[item].url
|
items.push({
|
||||||
let to = dest + '/' + encodeURIComponent(this.req.items[item].name)
|
from: this.req.items[item].url,
|
||||||
to = to.replace('//', '/')
|
to: this.dest + encodeURIComponent(this.req.items[item].name)
|
||||||
|
})
|
||||||
promises.push(api.move(from, to))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the promises.
|
// Execute the promises.
|
||||||
Promise.all(promises)
|
api.move(items)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.done('move')
|
buttons.done('move')
|
||||||
this.$router.push({ path: dest })
|
this.$router.push({ path: this.dest })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
buttons.done('move')
|
buttons.done('move')
|
||||||
this.$store.commit('showError', error)
|
this.$store.commit('showError', error)
|
||||||
})
|
})
|
||||||
},
|
|
||||||
fillOptions (req) {
|
|
||||||
// Sets the current path and resets
|
|
||||||
// the current items.
|
|
||||||
this.current = req.url
|
|
||||||
this.items = []
|
|
||||||
|
|
||||||
// If the path isn't the root path,
|
event.preventDefault()
|
||||||
// show a button to navigate to the previous
|
|
||||||
// directory.
|
|
||||||
if (req.url !== '/files/') {
|
|
||||||
this.items.push({
|
|
||||||
name: '..',
|
|
||||||
url: url.removeLastDir(req.url) + '/'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this folder is empty, finish here.
|
|
||||||
if (req.items === null) return
|
|
||||||
|
|
||||||
// Otherwise we add every directory to the
|
|
||||||
// move options.
|
|
||||||
for (let item of req.items) {
|
|
||||||
if (!item.isDir) continue
|
|
||||||
|
|
||||||
this.items.push({
|
|
||||||
name: item.name,
|
|
||||||
url: item.url
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showError (error) {
|
|
||||||
this.$store.commit('showError', error)
|
|
||||||
},
|
|
||||||
next: function (event) {
|
|
||||||
// Retrieves the URL of the directory the user
|
|
||||||
// just clicked in and fill the options with its
|
|
||||||
// content.
|
|
||||||
let uri = event.currentTarget.dataset.url
|
|
||||||
|
|
||||||
api.fetch(uri)
|
|
||||||
.then(this.fillOptions)
|
|
||||||
.catch(this.showError)
|
|
||||||
},
|
|
||||||
touchstart (event) {
|
|
||||||
let url = event.currentTarget.dataset.url
|
|
||||||
|
|
||||||
// In 300 milliseconds, we shall reset the count.
|
|
||||||
setTimeout(() => {
|
|
||||||
this.touches.count = 0
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
// If the element the user is touching
|
|
||||||
// is different from the last one he touched,
|
|
||||||
// reset the count.
|
|
||||||
if (this.touches.id !== url) {
|
|
||||||
this.touches.id = url
|
|
||||||
this.touches.count = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.touches.count++
|
|
||||||
|
|
||||||
// If there is more than one touch already,
|
|
||||||
// open the next screen.
|
|
||||||
if (this.touches.count > 1) {
|
|
||||||
this.next(event)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
select: function (event) {
|
|
||||||
// If the element is already selected, unselect it.
|
|
||||||
if (this.moveTo === event.currentTarget.dataset.url) {
|
|
||||||
this.moveTo = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise select the element.
|
|
||||||
this.moveTo = event.currentTarget.dataset.url
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<delete v-else-if="showDelete"></delete>
|
<delete v-else-if="showDelete"></delete>
|
||||||
<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>
|
||||||
<error v-else-if="showError"></error>
|
<error v-else-if="showError"></error>
|
||||||
<success v-else-if="showSuccess"></success>
|
<success v-else-if="showSuccess"></success>
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ import Delete from './Delete'
|
||||||
import Rename from './Rename'
|
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 Error from './Error'
|
import Error from './Error'
|
||||||
import Success from './Success'
|
import Success from './Success'
|
||||||
import NewFile from './NewFile'
|
import NewFile from './NewFile'
|
||||||
|
@ -60,6 +62,7 @@ export default {
|
||||||
Download,
|
Download,
|
||||||
Success,
|
Success,
|
||||||
Move,
|
Move,
|
||||||
|
Copy,
|
||||||
NewFile,
|
NewFile,
|
||||||
NewDir,
|
NewDir,
|
||||||
Help
|
Help
|
||||||
|
@ -83,6 +86,7 @@ export default {
|
||||||
showDelete: function () { return this.show === 'delete' },
|
showDelete: function () { return this.show === 'delete' },
|
||||||
showRename: function () { return this.show === 'rename' },
|
showRename: function () { return this.show === 'rename' },
|
||||||
showMove: function () { return this.show === 'move' },
|
showMove: function () { return this.show === 'move' },
|
||||||
|
showCopy: function () { return this.show === 'copy' },
|
||||||
showNewFile: function () { return this.show === 'newFile' },
|
showNewFile: function () { return this.show === 'newFile' },
|
||||||
showNewDir: function () { return this.show === 'newDir' },
|
showNewDir: function () { return this.show === 'newDir' },
|
||||||
showDownload: function () { return this.show === 'download' },
|
showDownload: function () { return this.show === 'download' },
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default {
|
||||||
this.name = encodeURIComponent(this.name)
|
this.name = encodeURIComponent(this.name)
|
||||||
newLink = url.removeLastDir(oldLink) + '/' + this.name
|
newLink = url.removeLastDir(oldLink) + '/' + this.name
|
||||||
|
|
||||||
api.move(oldLink, newLink)
|
api.move([{ from: oldLink, to: newLink }])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (this.req.kind !== 'listing') {
|
if (this.req.kind !== 'listing') {
|
||||||
this.$router.push({ path: newLink })
|
this.$router.push({ path: newLink })
|
||||||
|
|
|
@ -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: 16em;
|
max-width: 18em;
|
||||||
}
|
}
|
||||||
#file-selection .action {
|
#file-selection .action {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt div {
|
.prompt div:last-of-type {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
|
@ -9,6 +9,10 @@ const state = {
|
||||||
user: {},
|
user: {},
|
||||||
req: {},
|
req: {},
|
||||||
plugins: window.plugins || [],
|
plugins: window.plugins || [],
|
||||||
|
clipboard: {
|
||||||
|
key: '',
|
||||||
|
items: []
|
||||||
|
},
|
||||||
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
|
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
|
||||||
jwt: '',
|
jwt: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
|
@ -40,6 +40,14 @@ const mutations = {
|
||||||
},
|
},
|
||||||
updateRequest: (state, value) => {
|
updateRequest: (state, value) => {
|
||||||
state.req = value
|
state.req = value
|
||||||
|
},
|
||||||
|
updateClipboard: (state, value) => {
|
||||||
|
state.clipboard.key = value.key
|
||||||
|
state.clipboard.items = value.items
|
||||||
|
},
|
||||||
|
resetClipboard: (state) => {
|
||||||
|
state.clipboard.key = ''
|
||||||
|
state.clipboard.items = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,27 +99,45 @@ function put (url, content = '') {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function move (oldLink, newLink) {
|
function moveCopy (items, copy = false) {
|
||||||
oldLink = removePrefix(oldLink)
|
let promises = []
|
||||||
newLink = removePrefix(newLink)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
for (let item of items) {
|
||||||
let request = new window.XMLHttpRequest()
|
let from = removePrefix(item.from)
|
||||||
request.open('PATCH', `${store.state.baseURL}/api/resource${oldLink}`, true)
|
let to = removePrefix(item.to)
|
||||||
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
|
||||||
request.setRequestHeader('Destination', newLink)
|
|
||||||
|
|
||||||
request.onload = () => {
|
promises.push(new Promise((resolve, reject) => {
|
||||||
if (request.status === 200) {
|
let request = new window.XMLHttpRequest()
|
||||||
resolve(request.responseText)
|
request.open('PATCH', `${store.state.baseURL}/api/resource${from}`, true)
|
||||||
} else {
|
request.setRequestHeader('Authorization', `Bearer ${store.state.jwt}`)
|
||||||
reject(request.responseText)
|
request.setRequestHeader('Destination', to)
|
||||||
|
|
||||||
|
if (copy) {
|
||||||
|
request.setRequestHeader('Action', 'copy')
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
request.onerror = (error) => reject(error)
|
request.onload = () => {
|
||||||
request.send()
|
if (request.status === 200) {
|
||||||
})
|
resolve(request.responseText)
|
||||||
|
} else {
|
||||||
|
reject(request.responseText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = (error) => reject(error)
|
||||||
|
request.send()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
function move (items) {
|
||||||
|
return moveCopy(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy (items) {
|
||||||
|
return moveCopy(items, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function checksum (url, algo) {
|
function checksum (url, algo) {
|
||||||
|
@ -425,6 +443,7 @@ export default {
|
||||||
checksum,
|
checksum,
|
||||||
move,
|
move,
|
||||||
put,
|
put,
|
||||||
|
copy,
|
||||||
post,
|
post,
|
||||||
command,
|
command,
|
||||||
search,
|
search,
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -109,7 +109,11 @@ func (d Dir) Copy(src, dst string) error {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := d.Stat(src)
|
if dst == src {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
9
file.go
9
file.go
|
@ -82,7 +82,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
i := &file{
|
i := &file{
|
||||||
URL: "/files" + url.Path,
|
URL: "/files" + url.String(),
|
||||||
VirtualPath: url.Path,
|
VirtualPath: url.Path,
|
||||||
Path: filepath.Join(string(u.FileSystem), url.Path),
|
Path: filepath.Join(string(u.FileSystem), url.Path),
|
||||||
}
|
}
|
||||||
|
@ -127,6 +127,11 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||||
dirCount, fileCount int
|
dirCount, fileCount int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
baseurl, err := url.PathUnescape(i.URL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
name := f.Name()
|
name := f.Name()
|
||||||
allowed := c.User.Allowed("/" + name)
|
allowed := c.User.Allowed("/" + name)
|
||||||
|
@ -143,7 +148,7 @@ func (i *file) getListing(c *RequestContext, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Absolute URL
|
// Absolute URL
|
||||||
url := url.URL{Path: i.URL + name}
|
url := url.URL{Path: baseurl + name}
|
||||||
|
|
||||||
i := &file{
|
i := &file{
|
||||||
Name: f.Name(),
|
Name: f.Name(),
|
||||||
|
|
15
resource.go
15
resource.go
|
@ -13,11 +13,18 @@ import (
|
||||||
"github.com/hacdias/filemanager/dir"
|
"github.com/hacdias/filemanager/dir"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
// sanitizeURL sanitizes the URL to prevent path transversal
|
||||||
r.URL.Path = dir.SlashClean(r.URL.Path)
|
// using dir.SlashClean and adds the trailing slash bar.
|
||||||
if !c.User.Allowed(r.URL.Path) {
|
func sanitizeURL(url string) string {
|
||||||
return http.StatusForbidden, nil
|
path := dir.SlashClean(url)
|
||||||
|
if strings.HasSuffix(url, "/") && path != "/" {
|
||||||
|
return path + "/"
|
||||||
}
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
r.URL.Path = sanitizeURL(r.URL.Path)
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
17ca832c4f21ad3498dc9929167ddc6d372b3df1
|
|
Loading…
Reference in New Issue