diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java index 5133781348e9d779e2bed0582d642b4bdf168a75..f4cb5f1d7779ef47bc38315adf5c5d5dca8f8573 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java @@ -210,6 +210,7 @@ public class NodesController extends BaseController { @PostMapping(value = "/moveOrCopy") public ResponseEntity<List<Job>> moveOrCopyNodes(@RequestBody MoveOrCopyRequest request) throws Exception { + // Creates a transfer request for each copy or move operation CompletableFuture<JobSummary>[] futureJobs = request.getTargets().stream().map(t -> { String target = urlEncodePath(t); String direction = urlEncodePath(request.getDirection()); @@ -220,12 +221,21 @@ public class NodesController extends BaseController { return CompletableFuture.supplyAsync(() -> client.startTransferJob(transfer), Runnable::run); }).collect(Collectors.toList()).toArray(CompletableFuture[]::new); + // starts all HTTP requests in parallel CompletableFuture.allOf(futureJobs).join(); + // parses all the responses and populates a list of jobs to check the + // completion status using polling mechanism List<JobSummary> jobs = Stream.of(futureJobs).map(j -> j.join()).collect(Collectors.toList()); + // Job statuses are checked for some time and if completion takes too much + // time sends the execution phase to the UI and let it handles the polling. + // Since CompletableFutures can't really be aborted (see cancel method + // documentation) an AtomicReference containing a boolean flag it is + // passed to handle polling abortion. AtomicReference<Boolean> cancelled = new AtomicReference<>(false); + // CompletableFuture that triggers the timeout CompletableFuture timeout = CompletableFuture.runAsync(() -> { try { Thread.sleep(pollingTimeout * 1000); @@ -235,6 +245,7 @@ public class NodesController extends BaseController { }); try { + // performs polling of job statuses or timeout CompletableFuture.anyOf(jobsPolling(jobs, cancelled), timeout).join(); } catch (CompletionException ex) { if (ex.getCause() != null && ex.getCause() instanceof VOSpaceException) { @@ -243,8 +254,6 @@ public class NodesController extends BaseController { throw ex; } - // Try to perform polling until completion. If it takes too much time - // sends the execution phase to the UI and let it handles the polling. Job.JobType type = request.isKeepBytes() ? Job.JobType.COPY : Job.JobType.MOVE; return ResponseEntity.ok(jobs.stream().map(j -> new Job(j, type)) diff --git a/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue b/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue index 2534b926cd5eca7f597a08edc51c901cd64f8e3b..5bf47a934ef7f984d5e5fa774422322579323f82 100644 --- a/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue +++ b/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue @@ -5,6 +5,12 @@ --> <template> <b-modal id="move-or-copy-modal" :title="title" :okTitle="okTitle" @show="afterShow" @ok.prevent="moveOrCopyNodes" :ok-disabled="!writable" size="lg"> + <b-form inline v-if="this.nodesToMoveOrCopy.length === 1 && this.moveOrCopy === 'copy'" class="mb-3"> + <label class="w-25" for="new-name-input">New name</label> + <b-form-input v-model.trim="newName" id="new-name-input" class="w-75" aria-describedby="new-name-input-feedback" :state="newNameState" v-on:input="resetNewNameError" @keydown.native.enter="moveOrCopyNodes"> + </b-form-input> + <b-form-invalid-feedback id="new-folder-name-input-feedback" class="text-right">{{newNameError}}</b-form-invalid-feedback> + </b-form> <ol class="breadcrumb"> <li class="breadcrumb-item" v-for="(item, i) in breadcrumbs" :key="i" :class="{ 'active' : item.active }"> <a href="#" @click.stop.prevent="breadcrumbClick(i)" v-if="!item.active">{{item.text}}</a> @@ -66,13 +72,31 @@ export default { } } return items; + }, + newNameState() { + if (this.newNameError) { + return false; + } + return null; + } + }, + data() { + return { + newName: null, + newNameError: null } }, methods: { afterShow() { // starts from parent path let firstSelectedNode = this.nodesToMoveOrCopy[0]; - this.$store.dispatch('openNodeInMoveOrCopyModal', firstSelectedNode.substring(0, firstSelectedNode.lastIndexOf('/'))); + let lastSlashPos = firstSelectedNode.lastIndexOf('/'); + this.$store.dispatch('openNodeInMoveOrCopyModal', firstSelectedNode.substring(0, lastSlashPos)); + if (this.nodesToMoveOrCopy.length === 1 && this.moveOrCopy === 'copy') { + this.newName = firstSelectedNode.substring(lastSlashPos + 1); + } else { + this.newName = null; + } }, breadcrumbClick(i) { let pathSplit = this.destinationPath.split('/'); @@ -84,14 +108,31 @@ export default { window.openNodeInMoveOrCopyModal(event, path); }, moveOrCopyNodes() { + let direction = this.destinationPath; + + if (this.nodesToMoveOrCopy.length === 1 && this.moveOrCopy === 'copy') { + if (!this.newName) { + this.newNameError = "Name is required"; + return; + } else if (/[<>?":\\/`|'*]/.test(this.newName)) { + this.newNameError = "Name contains an illegal character. Following characters are not allowed: < > ? \" : \\ / | ' * `"; + return; + } + + direction += '/' + this.newName; + } + this.$store.dispatch('moveOrCopyNodes', { targets: this.nodesToMoveOrCopy, - direction: this.destinationPath, + direction, keepBytes: this.moveOrCopy === 'copy' }) .then(() => { this.$bvModal.hide('move-or-copy-modal'); }) + }, + resetNewNameError() { + this.newNameError = null; } } }