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>&nbsp;";
+        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>
+            &nbsp;
+            <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>