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 9f406d101c53266166ea6321a8040989ac99d672..5b94a115a05770a940fdd7a89085c7998ec81fc7 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 @@ -29,4 +29,9 @@ public class NodesController { nodesService.generateNodesHtml(path, response.getOutputStream()); } + + @GetMapping(value = "/download/{path}") + public void directDownload(@PathVariable("path") String path) { + // TODO: call pullFromVoSpace sync transfer + } } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..895b084682923de987d8d838e91a4fc1b2596917 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java @@ -0,0 +1,127 @@ +package it.inaf.ia2.vospace.ui.service; + +import it.inaf.ia2.vospace.ui.VOSpaceException; +import java.util.Optional; +import net.ivoa.xml.vospace.v2.Node; +import net.ivoa.xml.vospace.v2.Property; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NodeInfo { + + private static final Logger LOG = LoggerFactory.getLogger(NodeInfo.class); + + private final String authority; + + private final Node node; + + private final String path; + private final String name; + private final String size; + private final String groupRead; + private final String groupWrite; + private final boolean isPublic; + + public NodeInfo(Node node, String authority) { + this.authority = authority; + this.node = node; + this.path = getPath(node); + this.name = path.substring(path.lastIndexOf("/") + 1); + this.size = getSize(node); + this.groupRead = getGroupRead(node); + this.groupWrite = getGroupWrite(node); + this.isPublic = isPublic(node); + } + + private String getPath(Node node) { + + String uri = node.getUri(); + + String prefix = "vos://" + authority; + + if (!uri.startsWith(prefix)) { + throw new VOSpaceException("Node authority is different from configured one! Configured is " + authority + ", but node URI is " + uri); + } + + return uri.substring(prefix.length()); + } + + private String getGroupRead(Node node) { + return getProperty(node, "ivo://ivoa.net/vospace/core#groupread").orElse(""); + } + + private String getGroupWrite(Node node) { + return getProperty(node, "ivo://ivoa.net/vospace/core#groupwrite").orElse(""); + } + + private boolean isPublic(Node node) { + return Boolean.parseBoolean(getProperty(node, "ivo://ivoa.net/vospace/core#ispublic").orElse("false")); + } + + private Optional<String> getProperty(Node node, String uri) { + if (node.getProperties() != null && node.getProperties().getProperty() != null) { + for (Property property : node.getProperties().getProperty()) { + if (uri.equals(property.getUri())) { + return Optional.of(property.getValue()); + } + } + } + return Optional.empty(); + } + + private String getSize(Node node) { + return getProperty(node, "ivo://ivoa.net/vospace/core#length") + .map(value -> { + try { + long bytes = Long.parseLong(value); + return getHumanReadableSize(bytes); + } catch (NumberFormatException ex) { + LOG.warn("Invalid length for node " + node.getUri() + ". Length is " + value); + return ""; + } + }) + .orElse(""); + } + + /** + * Credits: https://stackoverflow.com/a/16576773/771431 + */ + private String getHumanReadableSize(long bytes) { + int u = 0; + for (; bytes > 1024 * 1024; bytes >>= 10) { + u++; + } + if (bytes > 1024) { + u++; + } + return String.format("%.1f %cB", bytes / 1024f, " kMGTPE".charAt(u)); + } + + public String getType() { + return node.getType(); + } + + public String getPath() { + return path; + } + + public String getName() { + return name; + } + + public String getSize() { + return size; + } + + public String getGroupRead() { + return groupRead; + } + + public String getGroupWrite() { + return groupWrite; + } + + public boolean isPublic() { + return isPublic; + } +} diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java index 96c21b822d6f80ea45380cdbc5c960242891d178..524ea4fc2f25e8620bee7b47f54667243b158fbd 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java @@ -1,11 +1,12 @@ package it.inaf.ia2.vospace.ui.service; -import it.inaf.ia2.vospace.ui.VOSpaceException; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import java.io.OutputStream; import java.io.PrintWriter; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Node; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -13,6 +14,8 @@ import org.springframework.stereotype.Service; @Service public class NodesService { + private static final Logger LOG = LoggerFactory.getLogger(NodesService.class); + @Autowired private VOSpaceClient client; @@ -37,24 +40,46 @@ public class NodesService { } private String getNodeHtml(Node node) { + + NodeInfo nodeInfo = new NodeInfo(node, authority); + String html = "<tr>"; - html += "<td><a href=\"#\">"; - html += getName(node); - html += "</a></td>"; + html += "<td><input type=\"checkbox\" data-node=\"" + nodeInfo.getPath() + "\" /></td>"; + html += "<td>" + getIcon(nodeInfo) + getLink(nodeInfo) + "</td>"; + html += "<td>" + nodeInfo.getSize() + "</td>"; + html += "<td>" + nodeInfo.getGroupRead() + "</td>"; + html += "<td>" + nodeInfo.getGroupWrite() + "</td>"; html += "</tr>"; return html; } - private String getName(Node node) { - - String uri = node.getUri(); - - String prefix = "vos://" + authority; + private String getIcon(NodeInfo nodeInfo) { + String html = "<span class=\"icon "; + if ("vos:ContainerNode".equals(nodeInfo.getType())) { + html += "folder"; + } else { + html += "file"; + } + html += "-icon\"></span> "; + return html; + } - if (!uri.startsWith(prefix)) { - throw new VOSpaceException("Node authority is different from configured one! Configured is " + authority + ", but node URI is " + uri); + private String getLink(NodeInfo nodeInfo) { + if (isDownloadable(nodeInfo)) { + if ("vos:ContainerNode".equals(nodeInfo.getType())) { + return "<a href=\"#nodes" + nodeInfo.getPath() + "\">" + nodeInfo.getName() + "</a>"; + } else { + return "<a href=\"download" + nodeInfo.getPath() + "\" target=\"blank_\">" + nodeInfo.getName() + "</a>"; + } } + return nodeInfo.getName(); + } - return uri.substring(prefix.length() + 1); + private boolean isDownloadable(NodeInfo nodeInfo) { + if (nodeInfo.isPublic()) { + return true; + } + // TODO: check user group + return false; } } diff --git a/vospace-ui-frontend/src/assets/css/fonts.css b/vospace-ui-frontend/src/assets/css/fonts.css new file mode 100644 index 0000000000000000000000000000000000000000..c59766b1687a6a4eab3c1b54de0f9968aa116578 --- /dev/null +++ b/vospace-ui-frontend/src/assets/css/fonts.css @@ -0,0 +1,14 @@ +.icon { + display: inline-block; + width: 1em; + height: 1em; + vertical-align: -0.15em; +} + +.folder-icon { + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='folder' xmlns='http://www.w3.org/2000/svg' fill='currentColor' %3E%3Cg%3E%3Cpath fill-rule='evenodd' d='M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v.64c.57.265.94.876.856 1.546l-.64 5.124A2.5 2.5 0 0 1 12.733 15H3.266a2.5 2.5 0 0 1-2.481-2.19l-.64-5.124A1.5 1.5 0 0 1 1 6.14V3.5zM2 6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5a.5.5 0 0 0-.5.5V6zm-.367 1a.5.5 0 0 0-.496.562l.64 5.124A1.5 1.5 0 0 0 3.266 14h9.468a1.5 1.5 0 0 0 1.489-1.314l.64-5.124A.5.5 0 0 0 14.367 7H1.633z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); +} + +.file-icon { + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='file earmark' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-file-earmark b-icon bi'%3E%3Cg%3E%3Cpath d='M4 0h5.5v1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h1V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z'%3E%3C/path%3E%3Cpath d='M9.5 3V0L14 4.5h-3A1.5 1.5 0 0 1 9.5 3z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); +} diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue index 482dffd8d8f3c1736916ac0a52d594e139646aa3..28ac7988173ebcd63aaac9e2c2e7e15caf0924c8 100644 --- a/vospace-ui-frontend/src/components/Main.vue +++ b/vospace-ui-frontend/src/components/Main.vue @@ -4,7 +4,15 @@ <table class="table b-table table-striped table-hover"> <thead> <tr> - <th>File</th> + <th id="checkboxes"> + <a href="#" @click.stop.prevent="selectAll"><BIconCheckSquare /></a> + + <a href="#" @click.stop.prevent="deSelectAll"><BIconSquare /></a> + </th> + <th>Name</th> + <th>Size</th> + <th>Group read</th> + <th>Group write</th> </tr> </thead> <tbody id="nodes"></tbody> @@ -15,13 +23,42 @@ <script> import client from 'api-client'; +import { BIconCheckSquare, BIconSquare } from 'bootstrap-vue' export default { + components: { + BIconCheckSquare, BIconSquare + }, mounted() { client.getNode(this.$store.state.path) .then(res => { document.getElementById('nodes').outerHTML = res; }); + }, + methods: { + selectAll() { + this.selectInputs(true); + }, + deSelectAll() { + this.selectInputs(false); + }, + selectInputs(value) { + document.querySelectorAll('#nodes input').forEach(input => input.checked = value); + } } } </script> + +<style> +@import '../assets/css/fonts.css'; + +.table { + text-align: left; +} + +th#checkboxes { + /* Credits: https://stackoverflow.com/a/43615091/771431 */ + width:0.1%; + white-space: nowrap; +} +</style>