From eed9da1471723ed3fbe6c00b1d6362b1c5fd8b04 Mon Sep 17 00:00:00 2001 From: Ramires Viana <59319979+ramiresviana@users.noreply.github.com> Date: Wed, 15 Jul 2020 15:12:13 +0000 Subject: [PATCH] feat: file copy, move and paste conflict checking --- fileutils/file.go | 3 +- frontend/src/api/files.js | 12 +++--- frontend/src/components/files/Listing.vue | 32 +++++++++++--- frontend/src/components/files/ListingItem.vue | 42 ++++++++++++++++--- frontend/src/components/prompts/Copy.vue | 40 ++++++++++++++---- frontend/src/components/prompts/FileList.vue | 14 +------ frontend/src/components/prompts/Move.vue | 40 +++++++++++++----- http/resource.go | 6 +++ 8 files changed, 138 insertions(+), 51 deletions(-) diff --git a/fileutils/file.go b/fileutils/file.go index 1b1e6403..00549584 100644 --- a/fileutils/file.go +++ b/fileutils/file.go @@ -2,6 +2,7 @@ package fileutils import ( "io" + "os" "path/filepath" "github.com/spf13/afero" @@ -25,7 +26,7 @@ func CopyFile(fs afero.Fs, source, dest string) error { } // 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 { return err } diff --git a/frontend/src/api/files.js b/frontend/src/api/files.js index 135e9799..e57d11f7 100644 --- a/frontend/src/api/files.js +++ b/frontend/src/api/files.js @@ -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 = [] for (let item of items) { const from = removePrefix(item.from) 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')) } return Promise.all(promises) } -export function move (items) { - return moveCopy(items) +export function move (items, overwrite = false) { + return moveCopy(items, false, overwrite) } -export function copy (items) { - return moveCopy(items, true) +export function copy (items, overwrite = false) { + return moveCopy(items, true, overwrite) } export async function checksum (url, algo) { diff --git a/frontend/src/components/files/Listing.vue b/frontend/src/components/files/Listing.vue index d34d2f10..3e1f9ba3 100644 --- a/frontend/src/components/files/Listing.vue +++ b/frontend/src/components/files/Listing.vue @@ -261,23 +261,43 @@ export default { for (let item of this.$store.state.clipboard.items) { const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from const to = this.$route.path + item.name - items.push({ from, to }) + items.push({ from, to, name: item.name }) } if (items.length === 0) { return } - if (this.$store.state.clipboard.key === 'x') { - api.move(items).then(() => { + let action = (overwrite) => { + api.copy(items, overwrite).then(() => { this.$store.commit('setReload', true) }).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 } - api.copy(items).then(() => { - this.$store.commit('setReload', true) - }).catch(this.$showError) + action(false) }, resizeEvent () { // Update the columns size based on the window width. diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue index 2eca5779..7a0b512b 100644 --- a/frontend/src/components/files/ListingItem.vue +++ b/frontend/src/components/files/ListingItem.vue @@ -36,6 +36,7 @@ import { mapMutations, mapGetters, mapState } from 'vuex' import filesize from 'filesize' import moment from 'moment' import { files as api } from '@/api' +import * as upload from '@/utils/upload' export default { name: 'item', @@ -110,26 +111,55 @@ export default { el.style.opacity = 1 }, - drop: function (event) { + drop: async function (event) { if (!this.canDrop) return event.preventDefault() 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 = [] for (let i of this.selected) { items.push({ 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 }) + } + + let base = el.querySelector('.name').innerHTML + '/' + 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) + }).catch(this.$showError) } - api.move(items) - .then(() => { - this.$store.commit('setReload', true) + 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) { if (this.selectedCount !== 0) event.preventDefault() diff --git a/frontend/src/components/prompts/Copy.vue b/frontend/src/components/prompts/Copy.vue index 91471bf4..a548804d 100644 --- a/frontend/src/components/prompts/Copy.vue +++ b/frontend/src/components/prompts/Copy.vue @@ -28,6 +28,7 @@ import { mapState } from 'vuex' import FileList from './FileList' import { files as api } from '@/api' import buttons from '@/utils/buttons' +import * as upload from '@/utils/upload' export default { name: 'copy', @@ -42,25 +43,46 @@ export default { methods: { copy: async 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) + to: this.dest + encodeURIComponent(this.req.items[item].name), + name: this.req.items[item].name }) } - try { - await api.copy(items) - buttons.success('copy') - this.$router.push({ path: this.dest }) - } catch (e) { - buttons.done('copy') - this.$showError(e) + let action = async (overwrite) => { + buttons.loading('copy') + + await api.copy(items, overwrite).then(() => { + buttons.success('copy') + this.$router.push({ path: this.dest }) + }).catch((e) => { + buttons.done('copy') + 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) } } } diff --git a/frontend/src/components/prompts/FileList.vue b/frontend/src/components/prompts/FileList.vue index 5769d3b9..243f66d7 100644 --- a/frontend/src/components/prompts/FileList.vue +++ b/frontend/src/components/prompts/FileList.vue @@ -41,19 +41,7 @@ export default { } }, 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. - files.fetch(url.removeLastDir(this.$route.path)) - .then(this.fillOptions) - .catch(this.$showError) + this.fillOptions(this.req) }, methods: { fillOptions (req) { diff --git a/frontend/src/components/prompts/Move.vue b/frontend/src/components/prompts/Move.vue index c827ea48..57300df9 100644 --- a/frontend/src/components/prompts/Move.vue +++ b/frontend/src/components/prompts/Move.vue @@ -27,6 +27,7 @@ import { mapState } from 'vuex' import FileList from './FileList' import { files as api } from '@/api' import buttons from '@/utils/buttons' +import * as upload from '@/utils/upload' export default { name: 'move', @@ -41,26 +42,45 @@ export default { methods: { move: async function (event) { event.preventDefault() - buttons.loading('move') let items = [] for (let item of this.selected) { items.push({ 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 { - api.move(items) - buttons.success('move') - this.$router.push({ path: this.dest }) - } catch (e) { - buttons.done('move') - this.$showError(e) + let action = async (overwrite) => { + buttons.loading('move') + + await api.move(items, overwrite).then(() => { + buttons.success('move') + this.$router.push({ path: this.dest }) + }).catch((e) => { + buttons.done('move') + this.$showError(e) + }) } - event.preventDefault() + 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) } } } diff --git a/http/resource.go b/http/resource.go index 2fd6cf04..e3859549 100644 --- a/http/resource.go +++ b/http/resource.go @@ -148,6 +148,12 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request, 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 { switch action { // TODO: use enum