feat: file copy, move and paste conflict checking

This commit is contained in:
Ramires Viana 2020-07-15 15:12:13 +00:00
parent 9a2ebbabe2
commit eed9da1471
8 changed files with 138 additions and 51 deletions

View File

@ -2,6 +2,7 @@ package fileutils
import ( import (
"io" "io"
"os"
"path/filepath" "path/filepath"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -25,7 +26,7 @@ func CopyFile(fs afero.Fs, source, dest string) error {
} }
// Create the destination file. // Create the destination file.
dst, err := fs.Create(dest) dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil { if err != nil {
return err return err
} }

View File

@ -112,25 +112,25 @@ export async function post (url, content = '', overwrite = false, onupload) {
}) })
} }
function moveCopy (items, copy = false) { function moveCopy (items, copy = false, overwrite = false) {
let promises = [] let promises = []
for (let item of items) { for (let item of items) {
const from = removePrefix(item.from) const from = removePrefix(item.from)
const to = encodeURIComponent(removePrefix(item.to)) const to = encodeURIComponent(removePrefix(item.to))
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}` const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}`
promises.push(resourceAction(url, 'PATCH')) promises.push(resourceAction(url, 'PATCH'))
} }
return Promise.all(promises) return Promise.all(promises)
} }
export function move (items) { export function move (items, overwrite = false) {
return moveCopy(items) return moveCopy(items, false, overwrite)
} }
export function copy (items) { export function copy (items, overwrite = false) {
return moveCopy(items, true) return moveCopy(items, true, overwrite)
} }
export async function checksum (url, algo) { export async function checksum (url, algo) {

View File

@ -261,23 +261,43 @@ export default {
for (let item of this.$store.state.clipboard.items) { for (let item of this.$store.state.clipboard.items) {
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
const to = this.$route.path + item.name const to = this.$route.path + item.name
items.push({ from, to }) items.push({ from, to, name: item.name })
} }
if (items.length === 0) { if (items.length === 0) {
return return
} }
if (this.$store.state.clipboard.key === 'x') { let action = (overwrite) => {
api.move(items).then(() => { api.copy(items, overwrite).then(() => {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}).catch(this.$showError) }).catch(this.$showError)
}
if (this.$store.state.clipboard.key === 'x') {
action = (overwrite) => {
api.move(items, overwrite).then(() => {
this.$store.commit('setReload', true)
}).catch(this.$showError)
}
}
let conflict = upload.checkConflict(items, this.req.items)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
action(true)
}
})
return return
} }
api.copy(items).then(() => { action(false)
this.$store.commit('setReload', true)
}).catch(this.$showError)
}, },
resizeEvent () { resizeEvent () {
// Update the columns size based on the window width. // Update the columns size based on the window width.

View File

@ -36,6 +36,7 @@ import { mapMutations, mapGetters, mapState } from 'vuex'
import filesize from 'filesize' import filesize from 'filesize'
import moment from 'moment' import moment from 'moment'
import { files as api } from '@/api' import { files as api } from '@/api'
import * as upload from '@/utils/upload'
export default { export default {
name: 'item', name: 'item',
@ -110,26 +111,55 @@ export default {
el.style.opacity = 1 el.style.opacity = 1
}, },
drop: function (event) { drop: async function (event) {
if (!this.canDrop) return if (!this.canDrop) return
event.preventDefault() event.preventDefault()
if (this.selectedCount === 0) return if (this.selectedCount === 0) return
let el = event.target
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
el = el.parentElement
}
}
let items = [] let items = []
for (let i of this.selected) { for (let i of this.selected) {
items.push({ items.push({
from: this.req.items[i].url, from: this.req.items[i].url,
to: this.url + this.req.items[i].name to: this.url + this.req.items[i].name,
name: this.req.items[i].name
}) })
} }
api.move(items) let base = el.querySelector('.name').innerHTML + '/'
.then(() => { let path = this.$route.path + base
let baseItems = (await api.fetch(path)).items
let action = (overwrite) => {
api.move(items, overwrite).then(() => {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}).catch(this.$showError)
}
let conflict = upload.checkConflict(items, baseItems)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
action(true)
}
}) })
.catch(this.$showError)
return
}
action(false)
}, },
click: function (event) { click: function (event) {
if (this.selectedCount !== 0) event.preventDefault() if (this.selectedCount !== 0) event.preventDefault()

View File

@ -28,6 +28,7 @@ import { mapState } from 'vuex'
import FileList from './FileList' import FileList from './FileList'
import { files as api } from '@/api' import { files as api } from '@/api'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
import * as upload from '@/utils/upload'
export default { export default {
name: 'copy', name: 'copy',
@ -42,25 +43,46 @@ export default {
methods: { methods: {
copy: async function (event) { copy: async function (event) {
event.preventDefault() event.preventDefault()
buttons.loading('copy')
let items = [] 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) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name) to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
}) })
} }
try { let action = async (overwrite) => {
await api.copy(items) buttons.loading('copy')
await api.copy(items, overwrite).then(() => {
buttons.success('copy') buttons.success('copy')
this.$router.push({ path: this.dest }) this.$router.push({ path: this.dest })
} catch (e) { }).catch((e) => {
buttons.done('copy') buttons.done('copy')
this.$showError(e) this.$showError(e)
})
} }
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
action(true)
}
})
return
}
action(false)
} }
} }
} }

View File

@ -41,19 +41,7 @@ export default {
} }
}, },
mounted () { 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) this.fillOptions(this.req)
return
}
// Otherwise, we must be on a preview or editor
// so we fetch the data from the previous directory.
files.fetch(url.removeLastDir(this.$route.path))
.then(this.fillOptions)
.catch(this.$showError)
}, },
methods: { methods: {
fillOptions (req) { fillOptions (req) {

View File

@ -27,6 +27,7 @@ import { mapState } from 'vuex'
import FileList from './FileList' import FileList from './FileList'
import { files as api } from '@/api' import { files as api } from '@/api'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
import * as upload from '@/utils/upload'
export default { export default {
name: 'move', name: 'move',
@ -41,26 +42,45 @@ export default {
methods: { methods: {
move: async function (event) { move: async function (event) {
event.preventDefault() event.preventDefault()
buttons.loading('move')
let items = [] let items = []
for (let item of this.selected) { for (let item of this.selected) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name) to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
}) })
} }
try { let action = async (overwrite) => {
api.move(items) buttons.loading('move')
await api.move(items, overwrite).then(() => {
buttons.success('move') buttons.success('move')
this.$router.push({ path: this.dest }) this.$router.push({ path: this.dest })
} catch (e) { }).catch((e) => {
buttons.done('move') buttons.done('move')
this.$showError(e) this.$showError(e)
})
} }
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault() event.preventDefault()
this.$store.commit('closeHovers')
action(true)
}
})
return
}
action(false)
} }
} }
} }

View File

@ -148,6 +148,12 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
if r.URL.Query().Get("override") != "true" {
if _, err := d.user.Fs.Stat(dst); err == nil {
return http.StatusConflict, nil
}
}
err = d.RunHook(func() error { err = d.RunHook(func() error {
switch action { switch action {
// TODO: use enum // TODO: use enum