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

Implemented file upload

parent 7a33ad06
No related branches found
No related tags found
No related merge requests found
Pipeline #883 passed
Showing
with 289 additions and 15 deletions
......@@ -87,7 +87,7 @@ public class VOSpaceClient {
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")
.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;
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 java.util.Map;
import javax.servlet.http.HttpServletRequest;
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.Transfer;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NodesController {
public class NodesController extends BaseController {
@Value("${vospace-authority}")
private String authority;
......@@ -63,14 +63,14 @@ public class NodesController {
protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
transfer.getProtocols().add(protocol);
String url = client.getDownloadEndpoints(transfer).get(0).getEndpoint();
String url = client.getFileServiceEndpoints(transfer).get(0).getEndpoint();
HttpHeaders headers = new HttpHeaders();
headers.set("Location", url);
return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
}
@PostMapping(value = "/folder")
public void newFolder(@RequestBody Map<String, String> params) {
public void newFolder(@RequestBody Map<String, Object> params) {
String parentPath = getRequiredParam(params, "parentPath");
if (!parentPath.startsWith("/")) {
......@@ -81,14 +81,12 @@ public class NodesController {
ContainerNode node = new ContainerNode();
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) {
if (!params.containsKey(key)) {
throw new BadRequestException("Missing mandatory parameter " + key);
}
return params.get(key);
client.createNode(node);
}
/**
......
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 {
},
createFolder() {
return fetch({});
},
prepareForUpload() {
return fetch(['http://fileservice/upload']);
},
uploadFile() {
return fetch({});
}
}
......@@ -99,5 +99,29 @@ export default {
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 @@
<b-breadcrumb :items="breadcrumbs"></b-breadcrumb>
<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="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>
</div>
<b-card>
......@@ -29,18 +29,21 @@
</table>
</b-card>
<CreateFolderModal />
<UploadFilesModal />
</div>
</template>
<script>
import { BIconCheckSquare, BIconSquare } from 'bootstrap-vue'
import CreateFolderModal from './modal/CreateFolderModal.vue'
import UploadFilesModal from './modal/UploadFilesModal.vue'
export default {
components: {
BIconCheckSquare,
BIconSquare,
CreateFolderModal
CreateFolderModal,
UploadFilesModal
},
computed: {
breadcrumbs() {
......
<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>
<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"
......@@ -27,6 +27,9 @@ export default {
}
},
methods: {
afterShow: function() {
this.$refs.newFolderNameInput.focus();
},
reset() {
this.newFolderName = null;
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({
// Reload current node
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