Skip to content
Snippets Groups Projects
Commit 17eb0e40 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Implemented file upload

parent 7a33ad06
Branches
Tags
No related merge requests found
Pipeline #883 passed
Showing
with 289 additions and 15 deletions
...@@ -87,7 +87,7 @@ public class VOSpaceClient { ...@@ -87,7 +87,7 @@ public class VOSpaceClient {
return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class)); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class));
} }
public List<Protocol> getDownloadEndpoints(Transfer transfer) { public List<Protocol> getFileServiceEndpoints(Transfer transfer) {
HttpRequest request = getRequest("/synctrans") HttpRequest request = getRequest("/synctrans")
.header("Accept", useJson ? "application/json" : "text/xml") .header("Accept", useJson ? "application/json" : "text/xml")
......
package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.vospace.ui.exception.BadRequestException;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
public class BaseController {
@Autowired
private HttpSession session;
protected User getUser() {
return (User) session.getAttribute("user_data");
}
protected String getRequiredParam(Map<String, Object> params, String key) {
if (!params.containsKey(key)) {
throw new BadRequestException("Missing mandatory parameter " + key);
}
return (String) params.get(key);
}
protected <T> T getRequiredParam(Map<String, Object> params, String key, Class<T> type) {
if (!params.containsKey(key)) {
throw new BadRequestException("Missing mandatory parameter " + key);
}
return (T) params.get(key);
}
}
package it.inaf.ia2.vospace.ui.controller; package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.exception.BadRequestException;
import it.inaf.ia2.vospace.ui.service.NodesService; import it.inaf.ia2.vospace.ui.service.NodesService;
import java.util.Map; import java.util.Map;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Property;
import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Protocol;
import net.ivoa.xml.vospace.v2.Transfer; import net.ivoa.xml.vospace.v2.Transfer;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
...@@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody; ...@@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
public class NodesController { public class NodesController extends BaseController {
@Value("${vospace-authority}") @Value("${vospace-authority}")
private String authority; private String authority;
...@@ -63,14 +63,14 @@ public class NodesController { ...@@ -63,14 +63,14 @@ public class NodesController {
protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
transfer.getProtocols().add(protocol); transfer.getProtocols().add(protocol);
String url = client.getDownloadEndpoints(transfer).get(0).getEndpoint(); String url = client.getFileServiceEndpoints(transfer).get(0).getEndpoint();
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Location", url); headers.set("Location", url);
return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER); return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
} }
@PostMapping(value = "/folder") @PostMapping(value = "/folder")
public void newFolder(@RequestBody Map<String, String> params) { public void newFolder(@RequestBody Map<String, Object> params) {
String parentPath = getRequiredParam(params, "parentPath"); String parentPath = getRequiredParam(params, "parentPath");
if (!parentPath.startsWith("/")) { if (!parentPath.startsWith("/")) {
...@@ -81,14 +81,12 @@ public class NodesController { ...@@ -81,14 +81,12 @@ public class NodesController {
ContainerNode node = new ContainerNode(); ContainerNode node = new ContainerNode();
node.setUri("vos://" + authority + parentPath + "/" + name); node.setUri("vos://" + authority + parentPath + "/" + name);
client.createNode(node); Property creator = new Property();
} creator.setUri("ivo://ivoa.net/vospace/core#creator");
creator.setValue(getUser().getName());
node.getProperties().add(creator);
private String getRequiredParam(Map<String, String> params, String key) { client.createNode(node);
if (!params.containsKey(key)) {
throw new BadRequestException("Missing mandatory parameter " + key);
}
return params.get(key);
} }
/** /**
......
package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.UploadFilesData;
import it.inaf.ia2.vospace.ui.exception.PermissionDeniedException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.validation.Valid;
import net.ivoa.xml.vospace.v2.DataNode;
import net.ivoa.xml.vospace.v2.Property;
import net.ivoa.xml.vospace.v2.Protocol;
import net.ivoa.xml.vospace.v2.Transfer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UploadController extends BaseController {
@Value("${vospace-authority}")
private String authority;
@Autowired
private VOSpaceClient client;
@PostMapping(value = "/preupload", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<String>> prepareForUpload(@RequestBody @Valid UploadFilesData data) {
if (getUser() == null) {
throw new PermissionDeniedException("File upload not allowed to anonymous users");
}
CompletableFuture<String>[] calls
= data.getFiles().stream().map(fileName -> prepareForDownload(getParentPath(data), fileName))
.toArray(CompletableFuture[]::new);
List<String> uploadUrls = CompletableFuture.allOf(calls)
.thenApplyAsync(ignore -> {
return Arrays.stream(calls).map(c -> c.join()).collect(Collectors.toList());
}).join();
return ResponseEntity.ok(uploadUrls);
}
private String getParentPath(UploadFilesData data) {
String parentPath = data.getParentPath();
if (!parentPath.startsWith("/")) {
parentPath = "/" + parentPath;
}
return parentPath;
}
public CompletableFuture<String> prepareForDownload(String parentPath, String fileName) {
return CompletableFuture.supplyAsync(() -> {
String nodeUri = "vos://" + authority + parentPath + "/" + fileName;
createDataNode(nodeUri, getUser().getName());
return obtainUploadUrl(nodeUri);
}, Runnable::run); // Passing current thread Executor to CompletableFuture to avoid "No thread-bound request found" exception
}
private void createDataNode(String nodeUri, String userId) {
DataNode node = new DataNode();
node.setUri(nodeUri);
Property creator = new Property();
creator.setUri("ivo://ivoa.net/vospace/core#creator");
creator.setValue(userId);
node.getProperties().add(creator);
client.createNode(node);
}
private String obtainUploadUrl(String uri) {
Transfer transfer = new Transfer();
transfer.setDirection("pushToVoSpace");
transfer.setTarget(uri);
Protocol protocol = new Protocol();
protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
transfer.getProtocols().add(protocol);
return client.getFileServiceEndpoints(transfer).get(0).getEndpoint();
}
}
package it.inaf.ia2.vospace.ui.data;
import java.util.List;
import javax.validation.constraints.NotNull;
public class UploadFilesData {
@NotNull
private String parentPath;
@NotNull
private List<String> files;
public String getParentPath() {
return parentPath;
}
public void setParentPath(String parentPath) {
this.parentPath = parentPath;
}
public List<String> getFiles() {
return files;
}
public void setFiles(List<String> files) {
this.files = files;
}
}
package it.inaf.ia2.vospace.ui.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.FORBIDDEN)
public class PermissionDeniedException extends VOSpaceException {
public PermissionDeniedException(String message) {
super(message);
}
}
...@@ -48,5 +48,11 @@ export default { ...@@ -48,5 +48,11 @@ export default {
}, },
createFolder() { createFolder() {
return fetch({}); return fetch({});
},
prepareForUpload() {
return fetch(['http://fileservice/upload']);
},
uploadFile() {
return fetch({});
} }
} }
...@@ -99,5 +99,29 @@ export default { ...@@ -99,5 +99,29 @@ export default {
name: newFolderName name: newFolderName
} }
}); });
},
prepareForUpload(path, files) {
let url = BASE_API_URL + 'preupload';
return apiRequest({
method: 'POST',
url: url,
withCredentials: true,
headers: {
'Cache-Control': 'no-cache'
},
data: {
parentPath: path,
files: files
}
});
},
uploadFile(url, file) {
let formData = new FormData();
formData.append('file', file);
axios.put(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
} }
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<b-breadcrumb :items="breadcrumbs"></b-breadcrumb> <b-breadcrumb :items="breadcrumbs"></b-breadcrumb>
<div class="mb-3"> <div class="mb-3">
<b-button variant="success" class="mr-2" :disabled="false" v-b-modal.create-folder-modal>New folder</b-button> <b-button variant="success" class="mr-2" :disabled="false" v-b-modal.create-folder-modal>New folder</b-button>
<b-button variant="success" class="mr-2" :disabled="true">Upload files</b-button> <b-button variant="success" class="mr-2" :disabled="false" v-b-modal.upload-files-modal>Upload files</b-button>
<b-button variant="primary" class="mr-2" v-if="tapeButtonEnabled" @click="startRecallFromTapeJob">Recall from tape</b-button> <b-button variant="primary" class="mr-2" v-if="tapeButtonEnabled" @click="startRecallFromTapeJob">Recall from tape</b-button>
</div> </div>
<b-card> <b-card>
...@@ -29,18 +29,21 @@ ...@@ -29,18 +29,21 @@
</table> </table>
</b-card> </b-card>
<CreateFolderModal /> <CreateFolderModal />
<UploadFilesModal />
</div> </div>
</template> </template>
<script> <script>
import { BIconCheckSquare, BIconSquare } from 'bootstrap-vue' import { BIconCheckSquare, BIconSquare } from 'bootstrap-vue'
import CreateFolderModal from './modal/CreateFolderModal.vue' import CreateFolderModal from './modal/CreateFolderModal.vue'
import UploadFilesModal from './modal/UploadFilesModal.vue'
export default { export default {
components: { components: {
BIconCheckSquare, BIconCheckSquare,
BIconSquare, BIconSquare,
CreateFolderModal CreateFolderModal,
UploadFilesModal
}, },
computed: { computed: {
breadcrumbs() { breadcrumbs() {
......
<template> <template>
<b-modal id="create-folder-modal" title="Create folder" okTitle="Create" @show="reset" @ok="createFolder"> <b-modal id="create-folder-modal" title="Create folder" okTitle="Create" @show="reset" @shown="afterShow" @ok="createFolder">
<b-form inline> <b-form inline>
<label class="w-25" for="new-folder-name-input">Folder name</label> <label class="w-25" for="new-folder-name-input">Folder name</label>
<b-form-input v-model.trim="newFolderName" id="new-folder-name-input" ref="newFolderNameInput" class="w-75" aria-describedby="new-folder-name-input-feedback" :state="newFolderNameState" v-on:input="resetError" <b-form-input v-model.trim="newFolderName" id="new-folder-name-input" ref="newFolderNameInput" class="w-75" aria-describedby="new-folder-name-input-feedback" :state="newFolderNameState" v-on:input="resetError"
...@@ -27,6 +27,9 @@ export default { ...@@ -27,6 +27,9 @@ export default {
} }
}, },
methods: { methods: {
afterShow: function() {
this.$refs.newFolderNameInput.focus();
},
reset() { reset() {
this.newFolderName = null; this.newFolderName = null;
this.resetError(); this.resetError();
......
<template>
<b-modal id="upload-files-modal" title="Upload file" okTitle="Upload" @show="reset" @ok="uploadFiles">
<b-form-file v-model="files" :multiple="true" :state="fileState" placeholder="Choose your files or drop them here..." drop-placeholder="Drop files here..."></b-form-file>
<b-form-invalid-feedback id="upload-file-input-feedback" class="text-right">{{uploadFileError}}</b-form-invalid-feedback>
<div class="mt-3">Selected files: {{ selectedFiles }}</div>
</b-modal>
</template>
<script>
export default {
data() {
return {
files: [],
uploadFileError: null
}
},
computed: {
fileState() {
if (this.uploadFileError) {
return false;
}
return null;
},
selectedFiles() {
if (this.files.length === 0) {
return '';
}
let names = [];
for (let file of this.files) {
names.push(file.name);
}
return names.join(', ');
}
},
methods: {
reset() {
this.files.splice(0, this.files.length);
this.resetError();
},
resetError() {
this.uploadFileError = null;
},
uploadFiles(event) {
// Prevent modal from closing
event.preventDefault();
if (this.files.length === 0) {
this.uploadFileError = "Select at least one file";
} else {
this.$store.dispatch('uploadFiles', this.files)
.then(() => {
this.$bvModal.hide('upload-files-modal');
});
}
}
}
}
</script>
...@@ -87,6 +87,22 @@ export default new Vuex.Store({ ...@@ -87,6 +87,22 @@ export default new Vuex.Store({
// Reload current node // Reload current node
dispatch('setPath', state.path); dispatch('setPath', state.path);
}); });
},
uploadFiles({ state, dispatch }, files) {
let names = [];
for (let file of files) {
names.push(file.name);
}
client.prepareForUpload(state.path, names)
.then(uploadUrls => {
for (let i = 0; i < files.length; i++) {
client.uploadFile(uploadUrls[i], files[i]);
}
})
.then(() => {
// Reload current node
dispatch('setPath', state.path);
});
} }
} }
}); });
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment