diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
index db0e40b7decd6c2e4976e48e57dca4f222bbf7ed..f251deba7529a664c3d2d05d70526b7884b644fd 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java
@@ -8,6 +8,7 @@ package it.inaf.ia2.vospace.ui;
 import it.inaf.ia2.aa.AuthConfig;
 import it.inaf.ia2.aa.LoginFilter;
 import it.inaf.ia2.aa.ServiceLocator;
+import it.inaf.ia2.aa.ServletRapClient;
 import it.inaf.ia2.aa.UserManager;
 import it.inaf.ia2.gms.client.GmsClient;
 import it.inaf.ia2.rap.client.ClientCredentialsRapClient;
@@ -86,4 +87,9 @@ public class VOSpaceUiApplication {
         String gmsBaseUri = ServiceLocator.getInstance().getConfig().getGmsUri();
         return new GmsClient(gmsBaseUri);
     }
+
+    @Bean
+    public ServletRapClient servletRapClient() {
+        return (ServletRapClient) ServiceLocator.getInstance().getRapClient();
+    }
 }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
index c4a3659c492987f2e44d702f561f1fb2ecea9883..d7c0d3cf096b0216c242c745ad2b815a2a5d1215 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
@@ -175,9 +175,20 @@ public class VOSpaceClient {
         return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class));
     }
 
-    public List<Job> getJobs() {
+    public List<Job> getAsyncRecallJobs() {
+        return getJobs("direction=pullToVoSpace", Job.JobType.ASYNC_RECALL);
+    }
+
+    public List<Job> getArchiveJobs() {
+        return getJobs("direction=pullFromVoSpace"
+                + "&VIEW=ivo://ia2.inaf.it/vospace/views%23tar"
+                + "&VIEW=ivo://ia2.inaf.it/vospace/views%23zip",
+                Job.JobType.ARCHIVE);
+    }
 
-        HttpRequest request = getRequest("/transfers?direction=pullToVoSpace")
+    private List<Job> getJobs(String queryString, Job.JobType type) {
+
+        HttpRequest request = getRequest("/transfers?" + queryString)
                 .header("Accept", useJson ? "application/json" : "text/xml")
                 .header("Content-Type", useJson ? "application/json" : "text/xml")
                 .GET()
@@ -185,11 +196,27 @@ public class VOSpaceClient {
 
         return call(request, BodyHandlers.ofInputStream(), 200, res -> {
             return unmarshal(res, Jobs.class).getJobref().stream()
-                    .map(jobDesc -> new Job(jobDesc))
+                    .map(jobDesc -> new Job(jobDesc, type))
                     .collect(Collectors.toList());
         });
     }
 
+    public String getArchiveJobHref(String jobId) {
+        List<Protocol> protocols = getTransferDetails(jobId).getProtocols();
+        if (!protocols.isEmpty()) {
+            return protocols.get(0).getEndpoint();
+        }
+        return null;
+    }
+
+    private Transfer getTransferDetails(String jobId) {
+
+        HttpRequest request = getRequest("/transfers/" + jobId + "/results/transferDetails")
+                .GET().build();
+
+        return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class));
+    }
+
     public ExecutionPhase getJobPhase(String jobId) {
 
         HttpRequest request = getRequest("/transfers/" + jobId + "/phase")
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/DownloadController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/DownloadController.java
new file mode 100644
index 0000000000000000000000000000000000000000..71fadfae413fb05b5439a80523e15ee77eb59660
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/DownloadController.java
@@ -0,0 +1,96 @@
+/*
+ * This file is part of vospace-ui
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.ia2.vospace.ui.controller;
+
+import it.inaf.ia2.aa.ServletRapClient;
+import it.inaf.ia2.aa.data.User;
+import it.inaf.ia2.rap.client.call.TokenExchangeRequest;
+import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
+import it.inaf.ia2.vospace.ui.exception.PermissionDeniedException;
+import it.inaf.oats.vospace.datamodel.NodeUtils;
+import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
+import java.util.Arrays;
+import javax.servlet.http.HttpServletRequest;
+import net.ivoa.xml.vospace.v2.Protocol;
+import net.ivoa.xml.vospace.v2.Transfer;
+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.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class DownloadController {
+
+    private static final Logger LOG = LoggerFactory.getLogger(DownloadController.class);
+
+    @Value("${vospace-authority}")
+    private String authority;
+
+    @Autowired
+    private VOSpaceClient client;
+
+    @Autowired
+    private HttpServletRequest servletRequest;
+
+    @Autowired
+    private ServletRapClient rapClient;
+
+    @GetMapping(value = "/download/**")
+    public ResponseEntity<?> directDownload() {
+
+        String path = getPath("/download/");
+        LOG.debug("directDownload called for path {}", path);
+
+        Transfer transfer = new Transfer();
+        transfer.setDirection("pullFromVoSpace");
+        transfer.setTarget(Arrays.asList("vos://" + authority + urlEncodePath(path)));
+
+        Protocol protocol = new Protocol();
+        protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
+        transfer.getProtocols().add(protocol);
+
+        String url = client.getFileServiceEndpoint(transfer);
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Location", url);
+        return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
+    }
+
+    @GetMapping(value = "/download")
+    public ResponseEntity<?> downloadJobResult(@RequestParam("jobId") String jobId) {
+
+        LOG.debug("job result download called for jobId {}", jobId);
+
+        String token = ((User) servletRequest.getUserPrincipal()).getAccessToken();
+
+        if (token == null) {
+            throw new PermissionDeniedException("Token is null");
+        }
+
+        String endpoint = client.getArchiveJobHref(jobId);
+
+        TokenExchangeRequest exchangeRequest = new TokenExchangeRequest()
+                .setSubjectToken(token)
+                .setResource(endpoint);
+
+        String newToken = rapClient.exchangeToken(exchangeRequest, servletRequest);
+
+        String url = endpoint + "?token=" + newToken;
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Location", url);
+        return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
+    }
+
+    protected String getPath(String prefix) {
+        String requestURL = servletRequest.getRequestURL().toString();
+        return NodeUtils.getPathFromRequestURLString(requestURL, prefix);
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java
index 3c560e5676f3079d392b885a80f267f1e7cbbe9c..352507fce507266a78c4d82a392210ee3b43fd45 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java
@@ -8,7 +8,9 @@ package it.inaf.ia2.vospace.ui.controller;
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
 import it.inaf.ia2.vospace.ui.data.Job;
 import it.inaf.ia2.vospace.ui.exception.BadRequestException;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
 import java.util.stream.Collectors;
 import net.ivoa.xml.uws.v1.ErrorType;
 import net.ivoa.xml.uws.v1.ExecutionPhase;
@@ -30,7 +32,7 @@ import org.springframework.web.bind.annotation.RestController;
 public class JobController extends BaseController {
 
     private static final Logger LOG = LoggerFactory.getLogger(JobController.class);
-    
+
     @Value("${vospace-authority}")
     private String authority;
 
@@ -55,11 +57,11 @@ public class JobController extends BaseController {
         transfer.getProtocols().add(protocol);
 
         JobSummary job = client.startTransferJob(transfer);
-        
+
         if (job.getPhase() == ExecutionPhase.QUEUED
                 || job.getPhase() == ExecutionPhase.PENDING
                 || job.getPhase() == ExecutionPhase.EXECUTING) {
-            return ResponseEntity.ok(new Job(job));
+            return ResponseEntity.ok(new Job(job, Job.JobType.ASYNC_RECALL));
         }
         String errorMessage;
         if (job.getPhase() == ExecutionPhase.ERROR) {
@@ -87,7 +89,22 @@ public class JobController extends BaseController {
     }
 
     @GetMapping(value = "/jobs", produces = MediaType.APPLICATION_JSON_VALUE)
-    public List<Job> getJobs() {
-        return client.getJobs();
+    public List<Job> getJobs() throws Exception {
+
+        CompletableFuture<List<Job>> asyncRecallJobsCall
+                = CompletableFuture.supplyAsync(() -> client.getAsyncRecallJobs(),
+                        Runnable::run);
+
+        CompletableFuture<List<Job>> archiveJobsCall
+                = CompletableFuture.supplyAsync(() -> client.getArchiveJobs(),
+                        Runnable::run);
+
+        CompletableFuture.allOf(asyncRecallJobsCall, archiveJobsCall).join();
+
+        List<Job> jobs = new ArrayList<>();
+        jobs.addAll(asyncRecallJobsCall.get());
+        jobs.addAll(archiveJobsCall.get());
+
+        return jobs;
     }
 }
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 5413860675d77eeceb1690e72448c0f0f51b8637..bdcda111a17fce30c84fb4f6567cc0e8c701af68 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
@@ -25,15 +25,15 @@ import net.ivoa.xml.uws.v1.ExecutionPhase;
 import net.ivoa.xml.uws.v1.JobSummary;
 import net.ivoa.xml.vospace.v2.ContainerNode;
 import net.ivoa.xml.vospace.v2.Node;
+import net.ivoa.xml.vospace.v2.Param;
 import net.ivoa.xml.vospace.v2.Property;
 import net.ivoa.xml.vospace.v2.Protocol;
 import net.ivoa.xml.vospace.v2.Transfer;
+import net.ivoa.xml.vospace.v2.View;
 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.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -52,7 +52,7 @@ public class NodesController extends BaseController {
 
     @Value("${maxPollingAttempts:10}")
     private int maxPollingAttempts;
-    
+
     @Autowired
     private VOSpaceClient client;
 
@@ -81,7 +81,7 @@ public class NodesController extends BaseController {
     public ResponseEntity<ListNodeData> listNodesForMoveModal(@RequestParam("path") String path, @RequestParam("nodeToMove") String nodeToMove, User principal) throws Exception {
 
         LOG.debug("listNodes called for path {}", path);
-        
+
         ListNodeData listNodeData = new ListNodeData();
 
         Node node = client.getNode(path);
@@ -94,26 +94,6 @@ public class NodesController extends BaseController {
         return ResponseEntity.ok(listNodeData);
     }
 
-    @GetMapping(value = "/download/**")
-    public ResponseEntity<?> directDownload() {
-
-        String path = getPath("/download/");
-        LOG.debug("directDownload called for path {}", path);
-
-        Transfer transfer = new Transfer();
-        transfer.setDirection("pullFromVoSpace");
-        transfer.setTarget(Arrays.asList("vos://" + authority + urlEncodePath(path)));
-
-        Protocol protocol = new Protocol();
-        protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
-        transfer.getProtocols().add(protocol);
-
-        String url = client.getFileServiceEndpoint(transfer);
-        HttpHeaders headers = new HttpHeaders();
-        headers.set("Location", url);
-        return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
-    }
-
     @PostMapping(value = "/folder")
     public void newFolder(@RequestBody Map<String, Object> params) {
 
@@ -162,6 +142,68 @@ public class NodesController extends BaseController {
         return ResponseEntity.noContent().build();
     }
 
+    @PostMapping(value = "/zip", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public Job createZip(@RequestBody List<String> paths) {
+        return createArchive(paths, "ivo://ia2.inaf.it/vospace/views#zip");
+    }
+
+    @PostMapping(value = "/tar", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public Job createTar(@RequestBody List<String> paths) {
+        return createArchive(paths, "ivo://ia2.inaf.it/vospace/views#tar");
+    }
+
+    private Job createArchive(List<String> paths, String viewUri) {
+
+        Transfer transfer = new Transfer();
+
+        View view = new View();
+        view.setUri(viewUri);
+        transfer.setView(view);
+
+        if (paths.size() == 1) {
+            transfer.setTarget(Arrays.asList("vos://" + authority + paths.get(0)));
+        } else {
+            String parent = getCommonParent(paths);
+            transfer.setTarget(Arrays.asList("vos://" + authority + parent));
+            for (String path : paths) {
+                String childName = path.substring(parent.length() + 1);
+                Param param = new Param();
+                param.setUri(viewUri + "/include");
+                param.setValue(childName);
+                view.getParam().add(param);
+            }
+        }
+
+        transfer.setDirection("pullFromVoSpace");
+        Protocol protocol = new Protocol();
+        protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
+        transfer.getProtocols().add(protocol);
+
+        return new Job(client.startTransferJob(transfer), Job.JobType.ARCHIVE);
+    }
+
+    private String getCommonParent(List<String> vosPaths) {
+        String commonParent = null;
+        for (String vosPath : vosPaths) {
+            if (commonParent == null) {
+                commonParent = vosPath;
+            } else {
+                StringBuilder newCommonParent = new StringBuilder();
+                boolean same = true;
+                int lastSlashPos = vosPath.lastIndexOf("/");
+                for (int i = 0; same && i < Math.min(commonParent.length(), vosPath.length()) && i < lastSlashPos; i++) {
+                    if (commonParent.charAt(i) == vosPath.charAt(i)) {
+                        newCommonParent.append(commonParent.charAt(i));
+                    } else {
+                        same = false;
+                    }
+                }
+                commonParent = newCommonParent.toString();
+            }
+        }
+        return commonParent;
+    }
+
     @PostMapping(value = "/move")
     public ResponseEntity<Job> moveNode(@RequestBody Map<String, Object> params) {
 
@@ -195,7 +237,7 @@ public class NodesController extends BaseController {
 
         job.setPhase(phase);
 
-        return ResponseEntity.ok(new Job(job));
+        return ResponseEntity.ok(new Job(job, Job.JobType.MOVE));
     }
 
     protected String getPath(String prefix) {
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java
index 6bd4ec4ad44e91c441b78e05301a840a056d8d1a..06ff9ed8b6d134528f91e5f0bdbc8ee0c1440271 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/Job.java
@@ -13,24 +13,33 @@ import net.ivoa.xml.uws.v1.ShortJobDescription;
 
 public class Job {
 
+    public static enum JobType {
+        ASYNC_RECALL,
+        ARCHIVE,
+        MOVE
+    }
+
     private String id;
     private String creationTime;
     private ExecutionPhase phase;
     private boolean read;
+    private JobType type;
 
     public Job() {
     }
 
-    public Job(JobSummary job) {
+    public Job(JobSummary job, JobType type) {
         this.id = job.getJobId();
         this.creationTime = formatCreationTime(job.getCreationTime());
         this.phase = job.getPhase();
+        this.type = type;
     }
 
-    public Job(ShortJobDescription job) {
+    public Job(ShortJobDescription job, JobType type) {
         this.id = job.getId();
         this.creationTime = formatCreationTime(job.getCreationTime());
         this.phase = job.getPhase();
+        this.type = type;
     }
 
     private String formatCreationTime(XMLGregorianCalendar calendar) {
@@ -72,4 +81,12 @@ public class Job {
     public void setRead(boolean read) {
         this.read = read;
     }
+
+    public JobType getType() {
+        return type;
+    }
+
+    public void setType(JobType type) {
+        this.type = type;
+    }
 }
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/DownloadControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/DownloadControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..51cd0e737fe7a200af29ca03aa2104ea387fd7fb
--- /dev/null
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/DownloadControllerTest.java
@@ -0,0 +1,69 @@
+/*
+ * This file is part of vospace-ui
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.ia2.vospace.ui.controller;
+
+import it.inaf.ia2.aa.ServletRapClient;
+import it.inaf.ia2.aa.data.User;
+import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.web.servlet.MockMvc;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc(addFilters = false)
+@TestPropertySource(properties = {"vospace-authority=example.com!vospace"})
+public class DownloadControllerTest {
+
+    @MockBean
+    private VOSpaceClient client;
+
+    @MockBean
+    private ServletRapClient rapClient;
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Test
+    public void testDirectDownload() throws Exception {
+
+        when(client.getFileServiceEndpoint(any())).thenReturn("http://redirect");
+
+        mockMvc.perform(get("/download/myfile"))
+                .andExpect(status().is3xxRedirection());
+
+        verify(client, times(1)).getFileServiceEndpoint(any());
+    }
+
+    @Test
+    public void testDownloadJobResult() throws Exception {
+
+        User user = mock(User.class);
+        when(user.getAccessToken()).thenReturn("<token>");
+
+        when(client.getArchiveJobHref("job123")).thenReturn("http://file-service/job123.zip");
+
+        when(rapClient.exchangeToken(any(), any())).thenReturn("<new-token>");
+
+        String redirect = mockMvc.perform(get("/download?jobId=job123")
+                .principal(user))
+                .andExpect(status().is3xxRedirection())
+                .andReturn().getResponse().getRedirectedUrl();
+
+        assertEquals("http://file-service/job123.zip?token=<new-token>", redirect);
+    }
+}
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java
index 4b6f6754b12efd903b100ff6393cb1f7e5e44132..b4209c93a626f42d65417a12de46b4956c4cf972 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java
@@ -6,6 +6,8 @@
 package it.inaf.ia2.vospace.ui.controller;
 
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
+import it.inaf.ia2.vospace.ui.data.Job;
+import java.util.List;
 import java.util.function.Consumer;
 import net.ivoa.xml.uws.v1.ErrorSummary;
 import net.ivoa.xml.uws.v1.ErrorType;
@@ -24,8 +26,10 @@ import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.http.MediaType;
 import org.springframework.test.context.TestPropertySource;
 import org.springframework.test.web.servlet.MockMvc;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import org.springframework.web.client.RestTemplate;
 
 @SpringBootTest
@@ -121,6 +125,19 @@ public class JobControllerTest {
         testErrorCall(ex -> assertTrue(ex.getMessage().contains("error")));
     }
 
+    @Test
+    public void testGetJobs() throws Exception {
+
+        when(client.getAsyncRecallJobs()).thenReturn(List.of(new Job(new JobSummary(), Job.JobType.ASYNC_RECALL)));
+        when(client.getArchiveJobs()).thenReturn(List.of(new Job(new JobSummary(), Job.JobType.ARCHIVE)));
+
+        mockMvc.perform(get("/jobs"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$").isArray())
+                .andExpect(jsonPath("$[0].type").value("ASYNC_RECALL"))
+                .andExpect(jsonPath("$[1].type").value("ARCHIVE"));
+    }
+
     private void testErrorCall(Consumer<Exception> exceptionChecker) throws Exception {
         try {
             mockMvc.perform(post("/recall")
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
index 013590ded3a774ae6ade34d97ff1db56f3e495b8..97d46f24d0e5533f52480e3cb3bda5588d8d9600 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
@@ -205,17 +205,6 @@ public class NodesControllerTest {
         }
     }
 
-    @Test
-    public void testDirectDownload() throws Exception {
-
-        when(client.getFileServiceEndpoint(any())).thenReturn("http://redirect");
-
-        mockMvc.perform(get("/download/myfile"))
-                .andExpect(status().is3xxRedirection());
-
-        verify(client, times(1)).getFileServiceEndpoint(any());
-    }
-
     @Test
     public void testNewFolder() throws Exception {
 
diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js
index ee4b9942f7573c25b70858c535352d12bb41957a..908567f36e59b156f2cc6f66216cbd0d326c1308 100644
--- a/vospace-ui-frontend/src/api/server/index.js
+++ b/vospace-ui-frontend/src/api/server/index.js
@@ -203,5 +203,17 @@ export default {
       },
       data
     }, true, true);
+  },
+  createArchive(paths, type) {
+    let url = BASE_API_URL + type;
+    return apiRequest({
+      method: 'POST',
+      url: url,
+      withCredentials: true,
+      headers: {
+        'Cache-Control': 'no-cache'
+      },
+      data: paths
+    }, true, true);
   }
 }
diff --git a/vospace-ui-frontend/src/components/Jobs.vue b/vospace-ui-frontend/src/components/Jobs.vue
index 3c54d8aea9b8a9286baa9964c2fd9d08424e60f7..c77089cb97f499dfe63929cf608114fc4112470d 100644
--- a/vospace-ui-frontend/src/components/Jobs.vue
+++ b/vospace-ui-frontend/src/components/Jobs.vue
@@ -6,23 +6,32 @@
 <template>
 <div>
   <b-button variant="primary" class="float-right" @click="loadJobs">Reload</b-button>
-  <h3>Async recall jobs</h3>
-  <table class="table b-table table-striped table-hover">
-    <thead>
-      <tr>
-        <th>Creation time</th>
-        <th>Id</th>
-        <th>Phase</th>
-      </tr>
-    </thead>
-    <tbody>
-      <tr v-for="job in jobs" :key="job.id">
-        <td>{{job.creationTime}}</td>
-        <td>{{job.id}}</td>
-        <td>{{job.phase}}</td>
-      </tr>
-    </tbody>
-  </table>
+  <h3>Jobs</h3>
+  <div v-if="jobs.length > 0" class="mb-3">
+    <table class="table b-table table-striped table-hover">
+      <thead>
+        <tr>
+          <th>Type</th>
+          <th>Creation time</th>
+          <th>Id</th>
+          <th>Link</th>
+          <th>Phase</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr v-for="job in jobs" :key="job.id">
+          <td>{{job.type}}</td>
+          <td>{{job.creationTime}}</td>
+          <td>{{job.id}}</td>
+          <td><a :href="'download?jobId=' + job.id" v-if="job.phase === 'COMPLETED' && job.type === 'ARCHIVE'">Download archive</a></td>
+          <td>{{job.phase}}</td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+  <div v-if="jobs.length === 0">
+    No jobs
+  </div>
   <div id="jobs-loading" v-if="jobsLoading" class="loading">
     <div class="spinner-wrapper">
       <b-spinner variant="primary" style="width: 3rem; height: 3rem;" label="Loading"></b-spinner>
diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue
index dfc5f2c1ccbe64ee47ae432b7a31aad350254bd9..bf2ff6122205165ac95eaca0b15b159af091855b 100644
--- a/vospace-ui-frontend/src/components/Main.vue
+++ b/vospace-ui-frontend/src/components/Main.vue
@@ -9,8 +9,12 @@
   <div class="mb-3">
     <b-button variant="success" class="mr-2" :disabled="!writable" v-b-modal.create-folder-modal>New folder</b-button>
     <b-button variant="success" class="mr-2" :disabled="!writable" v-b-modal.upload-files-modal>Upload files</b-button>
-    <b-button variant="primary" class="mr-2" v-if="asyncButtonEnabled" @click="startAsyncRecallJob">Async recall</b-button>
-    <b-button variant="danger" class="mr-2" v-if="deleteButtonEnabled" @click="deleteNodes">Delete</b-button>
+    <b-dropdown variant="primary" text="Actions" v-if="asyncButtonEnabled || deleteButtonEnabled || archiveButtonEnabled">
+      <b-dropdown-item :disabled="!asyncButtonEnabled" @click="startAsyncRecallJob">Async recall</b-dropdown-item>
+      <b-dropdown-item :disabled="!deleteButtonEnabled" @click="deleteNodes">Delete</b-dropdown-item>
+      <b-dropdown-item :disabled="!archiveButtonEnabled" @click="createArchive('zip')">Create zip archive</b-dropdown-item>
+      <b-dropdown-item :disabled="!archiveButtonEnabled" @click="createArchive('tar')">Create tar archive</b-dropdown-item>
+    </b-dropdown>
   </div>
   <b-card>
     <table class="table b-table table-striped table-hover">
@@ -41,6 +45,7 @@
   <ShareModal />
   <RenameModal />
   <MoveModal />
+  <ConfirmArchiveModal />
 </div>
 </template>
 
@@ -52,6 +57,7 @@ import ConfirmDeleteModal from './modal/ConfirmDeleteModal.vue'
 import ShareModal from './modal/ShareModal.vue'
 import RenameModal from './modal/RenameModal.vue'
 import MoveModal from './modal/MoveModal.vue'
+import ConfirmArchiveModal from './modal/ConfirmArchiveModal.vue'
 
 export default {
   components: {
@@ -62,7 +68,8 @@ export default {
     ConfirmDeleteModal,
     ShareModal,
     RenameModal,
-    MoveModal
+    MoveModal,
+    ConfirmArchiveModal
   },
   computed: {
     breadcrumbs() {
@@ -87,6 +94,9 @@ export default {
     deleteButtonEnabled() {
       return this.$store.state.deleteButtonEnabled;
     },
+    archiveButtonEnabled() {
+      return this.$store.state.archiveButtonEnabled;
+    },
     writable() {
       return this.$store.state.writable;
     }
@@ -118,11 +128,12 @@ export default {
     },
     deleteNodes() {
       let selectedNodesCheckboxes = document.querySelectorAll('#nodes input:checked');
-      let paths = [], unDeletablePaths = [];
+      let paths = [];
+      let unDeletablePaths = [];
       for (let i = 0; i < selectedNodesCheckboxes.length; i++) {
         let checkbox = selectedNodesCheckboxes[i];
         let dataNode = checkbox.getAttribute('data-node');
-        if(checkbox.classList.contains('deletable')) {
+        if (checkbox.classList.contains('deletable')) {
           paths.push(dataNode);
         } else {
           unDeletablePaths.push(dataNode);
@@ -131,6 +142,28 @@ export default {
       this.$store.commit('setNodesToDelete', paths);
       this.$store.commit('setSelectedUndeletableNodes', unDeletablePaths);
       this.$bvModal.show('confirm-delete-modal');
+    },
+    createArchive(type) {
+      let selectedNodesCheckboxes = document.querySelectorAll('#nodes input:checked');
+      let nodesToArchive = [];
+      let notArchivableNodes = [];
+      for (let i = 0; i < selectedNodesCheckboxes.length; i++) {
+        let checkbox = selectedNodesCheckboxes[i];
+        let dataNode = checkbox.getAttribute('data-node');
+        if (checkbox.classList.contains('async')) {
+          notArchivableNodes.push(dataNode);
+        } else {
+          nodesToArchive.push(dataNode);
+        }
+      }
+      this.$store.commit('setArchiveType', type);
+      this.$store.commit('setNodesToArchives', nodesToArchive);
+      if (notArchivableNodes.length === 0) {
+        this.$store.dispatch('createArchive', type)
+      } else {
+        this.$store.commit('setSelectedNotArchivableNodes', notArchivableNodes);
+        this.$bvModal.show('confirm-archive-modal');
+      }
     }
   }
 }
diff --git a/vospace-ui-frontend/src/components/modal/ConfirmArchiveModal.vue b/vospace-ui-frontend/src/components/modal/ConfirmArchiveModal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0329cf0d38bd1987f946861082e8c492d3d55c5d
--- /dev/null
+++ b/vospace-ui-frontend/src/components/modal/ConfirmArchiveModal.vue
@@ -0,0 +1,33 @@
+<!--
+  This file is part of vospace-ui
+  Copyright (C) 2021 Istituto Nazionale di Astrofisica
+  SPDX-License-Identifier: GPL-3.0-or-later
+-->
+<template>
+<b-modal id="confirm-archive-modal" title="Archive creation warning" okTitle="Ok, proceed anyway" @ok.prevent="createArchive" size="lg">
+  <p><strong>Warning</strong>: some of the nodes you selected require to be asynchronously retrieved before including them into an archive:</p>
+  <p>
+  <ul>
+    <li v-for="node in notArchivableNodes" :key="node">{{node}}</li>
+  </ul>
+  </p>
+  <p>If you proceed these files will be ignored and not included in the resulting archive file.</p>
+</b-modal>
+</template>
+
+<script>
+export default {
+  name: 'ConfirmArchiveModal',
+  computed: {
+    notArchivableNodes() { return this.$store.state.selectedNotArchivableNodes }
+  },
+  methods: {
+    createArchive() {
+      this.$store.dispatch('createArchive')
+        .then(() => {
+          this.$bvModal.hide('confirm-archive-modal');
+        });
+    }
+  }
+}
+</script>
diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js
index f610813da80b1d635279c78532cd69ce54749b9d..236a773286a3cbc9db2c1710e0849986d5148f3e 100644
--- a/vospace-ui-frontend/src/store.js
+++ b/vospace-ui-frontend/src/store.js
@@ -28,6 +28,7 @@ export default new Vuex.Store({
     nodesLoading: false,
     asyncButtonEnabled: false,
     deleteButtonEnabled: false,
+    archiveButtonEnabled: false,
     jobs: [],
     jobsLoading: true,
     lastJobsCheckTime: null,
@@ -43,7 +44,10 @@ export default new Vuex.Store({
     nodeToRename: null,
     nodeToMove: null,
     nodeToMoveDestination: null,
-    nodeToMoveDestinationWritable: false
+    nodeToMoveDestinationWritable: false,
+    archiveType: null,
+    nodesToArchive: [],
+    selectedNotArchivableNodes: []
   },
   mutations: {
     setLoading(state, loading) {
@@ -64,6 +68,9 @@ export default new Vuex.Store({
     setDeleteButtonEnabled(state, value) {
       state.deleteButtonEnabled = value;
     },
+    setArchiveButtonEnabled(state, value) {
+      state.archiveButtonEnabled = value;
+    },
     setJobs(state, jobs) {
       updateArray(state.jobs, jobs);
     },
@@ -101,6 +108,15 @@ export default new Vuex.Store({
     },
     setNodeToMoveDestinationWritable(state, value) {
       state.nodeToMoveDestinationWritable = value;
+    },
+    setArchiveType(state, type) {
+      state.archiveType = type;
+    },
+    setNodesToArchives(state, paths) {
+      state.nodesToArchive = paths;
+    },
+    setSelectedNotArchivableNodes(state, paths) {
+      state.selectedNotArchivableNodes = paths;
     }
   },
   actions: {
@@ -127,6 +143,7 @@ export default new Vuex.Store({
     computeButtonsVisibility({ commit }) {
       commit('setAsyncButtonEnabled', document.querySelectorAll('#nodes input.async:checked').length > 0);
       commit('setDeleteButtonEnabled', document.querySelectorAll('#nodes input.deletable:checked').length > 0);
+      commit('setArchiveButtonEnabled', document.querySelectorAll('#nodes input:not(.async):checked').length > 0);
     },
     startAsyncRecallJob({ state, commit, dispatch }) {
       let asyncCheckboxes = document.querySelectorAll('#nodes input.async:checked');
@@ -239,6 +256,17 @@ export default new Vuex.Store({
           commit('setNodeToMoveDestinationWritable', res.writable);
           document.getElementById('move-nodes').outerHTML = res.html;
         });
+    },
+    createArchive({ state, commit }) {
+      client.createArchive(state.nodesToArchive, state.archiveType)
+        .then(job => {
+          if (job.phase === 'ERROR') {
+            main.showError('Error creating ' + state.archiveType + ' archive');
+          } else {
+            main.showInfo(state.archiveType + ' creation started');
+            commit('addJob', job);
+          }
+        });
     }
   }
 });