feat: file copy, move and paste conflict checking
This commit is contained in:
		
							parent
							
								
									9a2ebbabe2
								
							
						
					
					
						commit
						eed9da1471
					
				|  | @ -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 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -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) { | ||||||
|  |  | ||||||
|  | @ -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. | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  |  | ||||||
|  | @ -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') | ||||||
|         buttons.success('copy') | 
 | ||||||
|         this.$router.push({ path: this.dest }) |         await api.copy(items, overwrite).then(() => { | ||||||
|       } catch (e) { |           buttons.success('copy') | ||||||
|         buttons.done('copy') |           this.$router.push({ path: this.dest }) | ||||||
|         this.$showError(e) |         }).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) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -41,19 +41,7 @@ export default { | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted () { |   mounted () { | ||||||
|     // If we're showing this on a listing, |     this.fillOptions(this.req) | ||||||
|     // 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) |  | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     fillOptions (req) { |     fillOptions (req) { | ||||||
|  |  | ||||||
|  | @ -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') | ||||||
|         buttons.success('move') | 
 | ||||||
|         this.$router.push({ path: this.dest }) |         await api.move(items, overwrite).then(() => { | ||||||
|       } catch (e) { |           buttons.success('move') | ||||||
|         buttons.done('move') |           this.$router.push({ path: this.dest }) | ||||||
|         this.$showError(e) |         }).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) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue