feat: file copy, move and paste conflict checking
This commit is contained in:
		
							parent
							
								
									9a2ebbabe2
								
							
						
					
					
						commit
						eed9da1471
					
				|  | @ -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 | ||||
| 	} | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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 | ||||
|         }) | ||||
|       }       | ||||
| 
 | ||||
|       api.move(items) | ||||
|         .then(() => { | ||||
|       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) | ||||
|       } | ||||
| 
 | ||||
|       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() | ||||
|  |  | |||
|  | @ -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) | ||||
|       let action = async (overwrite) => { | ||||
|         buttons.loading('copy') | ||||
| 
 | ||||
|         await api.copy(items, overwrite).then(() => { | ||||
|           buttons.success('copy') | ||||
|           this.$router.push({ path: this.dest }) | ||||
|       } catch (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 () { | ||||
|     // 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) | ||||
|   }, | ||||
|   methods: { | ||||
|     fillOptions (req) { | ||||
|  |  | |||
|  | @ -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) | ||||
|       let action = async (overwrite) => { | ||||
|         buttons.loading('move') | ||||
| 
 | ||||
|         await api.move(items, overwrite).then(() => { | ||||
|           buttons.success('move') | ||||
|           this.$router.push({ path: this.dest }) | ||||
|       } catch (e) { | ||||
|         }).catch((e) => { | ||||
|           buttons.done('move') | ||||
|           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) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue