diff --git a/pom.xml b/pom.xml
index 59a899333b52ff54c7a50344e2ca10c0e640dc31..fbe35e79e67a7707ae65efc7418272caa791ebbb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -148,6 +148,9 @@
             <plugin>
                 <artifactId>maven-surefire-plugin</artifactId>
                 <version>2.22.2</version>
+                <configuration>
+                    <trimStackTrace>false</trimStackTrace>
+                </configuration>
             </plugin>
             <plugin>
                 <groupId>org.springframework.boot</groupId>
diff --git a/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java
index 7adc30cd3d4d1b4382f7b787df12b5d75a4a23cb..4b85174cf004680f962e3b1baffa135f74481fc2 100644
--- a/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java
+++ b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java
@@ -7,15 +7,21 @@ package it.inaf.ia2.transfer.controller;
 
 import it.inaf.ia2.transfer.auth.TokenPrincipal;
 import it.inaf.ia2.transfer.service.ArchiveJob;
+import it.inaf.ia2.transfer.service.ArchiveJob.Type;
 import it.inaf.ia2.transfer.service.ArchiveService;
-import java.util.List;
+import java.io.File;
 import java.util.concurrent.CompletableFuture;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
+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;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
@@ -27,21 +33,36 @@ public class ArchiveFileController {
     @Autowired
     private HttpServletRequest request;
 
-    @PostMapping(value = "/tar", consumes = MediaType.APPLICATION_JSON_VALUE)
-    public void createTarArchive(@RequestParam(value = "jobId", required = true) String jobId, @RequestBody List<String> vosPaths) {
+    @Autowired
+    private HttpServletResponse response;
+
+    @PostMapping(value = "/archive", consumes = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<?> createArchiveFile(@RequestBody ArchiveRequest archiveRequest) {
+
+        Type type = Type.valueOf(archiveRequest.getType());
 
         ArchiveJob job = new ArchiveJob();
         job.setPrincipal((TokenPrincipal) request.getUserPrincipal());
-        job.setJobId(jobId);
-        job.setType(ArchiveJob.Type.TAR);
-        job.setVosPaths(vosPaths);
+        job.setJobId(archiveRequest.getJobId());
+        job.setType(type);
+        job.setVosPaths(archiveRequest.getPaths());
 
-        startArchiveJob(job);
-    }
-
-    private void startArchiveJob(ArchiveJob job) {
         CompletableFuture.runAsync(() -> {
             archiveService.createArchive(job);
         });
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Location", request.getRequestURL() + "/" + archiveRequest.getJobId() + "." + type.getExtension());
+        return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER);
+    }
+
+    @GetMapping(value = "/archive/{fileName}")
+    public ResponseEntity<?> getArchiveFile(@PathVariable("fileName") String fileName) {
+
+        TokenPrincipal principal = (TokenPrincipal) request.getUserPrincipal();
+
+        File file = archiveService.getArchiveParentDir(principal).toPath().resolve(fileName).toFile();
+
+        return FileResponseUtil.getFileResponse(response, file);
     }
 }
diff --git a/src/main/java/it/inaf/ia2/transfer/controller/ArchiveRequest.java b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveRequest.java
index 91cef3448b7dcc30ac5dd4f049a936232f5cc63c..09e3fe193ee9f84e19971236b56a1346be2337ec 100644
--- a/src/main/java/it/inaf/ia2/transfer/controller/ArchiveRequest.java
+++ b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveRequest.java
@@ -3,14 +3,37 @@
  * Copyright (C) 2021 Istituto Nazionale di Astrofisica
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
-
 package it.inaf.ia2.transfer.controller;
 
 import java.util.List;
 
 public class ArchiveRequest {
 
-    private List<String> vosPaths;
-    String jobId;
-    String type;
+    private String type;
+    private String jobId;
+    private List<String> paths;
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getJobId() {
+        return jobId;
+    }
+
+    public void setJobId(String jobId) {
+        this.jobId = jobId;
+    }
+
+    public List<String> getPaths() {
+        return paths;
+    }
+
+    public void setPaths(List<String> paths) {
+        this.paths = paths;
+    }
 }
diff --git a/src/main/java/it/inaf/ia2/transfer/controller/FileResponseUtil.java b/src/main/java/it/inaf/ia2/transfer/controller/FileResponseUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..225c048c9aef7fb1fa2a85aa342c5867271c4c30
--- /dev/null
+++ b/src/main/java/it/inaf/ia2/transfer/controller/FileResponseUtil.java
@@ -0,0 +1,59 @@
+/*
+ * This file is part of vospace-file-service
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.ia2.transfer.controller;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
+import static org.springframework.http.HttpStatus.NOT_FOUND;
+import org.springframework.http.ResponseEntity;
+
+public class FileResponseUtil {
+
+    private static final Logger LOG = LoggerFactory.getLogger(FileResponseUtil.class);
+
+    public static ResponseEntity<?> getFileResponse(HttpServletResponse response, File file) {
+        return getFileResponse(response, file, null);
+    }
+
+    public static ResponseEntity<?> getFileResponse(HttpServletResponse response, File file, String fileName) {
+
+        if (!file.exists()) {
+            LOG.error("File not found: " + file.getAbsolutePath());
+            return new ResponseEntity<>("File " + file.getName() + " not found", NOT_FOUND);
+        }
+
+        if (!file.canRead()) {
+            LOG.error("File not readable: " + file.getAbsolutePath());
+            return new ResponseEntity<>("File " + file.getName() + " is not readable", INTERNAL_SERVER_ERROR);
+        }
+
+        response.setHeader("Content-Disposition", "attachment; filename="
+                + URLEncoder.encode(fileName == null ? file.getName() : fileName, StandardCharsets.UTF_8));
+        response.setHeader("Content-Length", String.valueOf(file.length()));
+        response.setCharacterEncoding("UTF-8");
+
+        byte[] bytes = new byte[1024];
+        try ( OutputStream out = response.getOutputStream();  InputStream is = new FileInputStream(file)) {
+            int read;
+            while ((read = is.read(bytes)) != -1) {
+                out.write(bytes, 0, read);
+            }
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/it/inaf/ia2/transfer/controller/GetFileController.java b/src/main/java/it/inaf/ia2/transfer/controller/GetFileController.java
index be4e4fe28d8f1adcd206f386bf6adaa72a10abf1..c2481e24b5c2a815653bb95002b55c4637c87a51 100644
--- a/src/main/java/it/inaf/ia2/transfer/controller/GetFileController.java
+++ b/src/main/java/it/inaf/ia2/transfer/controller/GetFileController.java
@@ -10,19 +10,11 @@ import it.inaf.ia2.transfer.auth.TokenPrincipal;
 import it.inaf.ia2.transfer.persistence.FileDAO;
 import it.inaf.ia2.transfer.service.AuthorizationService;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UncheckedIOException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 import javax.servlet.http.HttpServletResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
 import static org.springframework.http.HttpStatus.NOT_FOUND;
 import static org.springframework.http.HttpStatus.UNAUTHORIZED;
 import org.springframework.http.ResponseEntity;
@@ -69,33 +61,9 @@ public class GetFileController extends FileController {
     private ResponseEntity<?> getFileResponse(FileInfo fileInfo) {
 
         File file = new File(fileInfo.getOsPath());
+        String vosName = fileInfo.getVirtualPath() == null ? null
+                : fileInfo.getVirtualPath().substring(fileInfo.getVirtualPath().lastIndexOf("/") + 1);
 
-        if (!file.exists()) {
-            LOG.error("File not found: " + file.getAbsolutePath());
-            return new ResponseEntity<>("File " + file.getName() + " not found", NOT_FOUND);
-        }
-
-        if (!file.canRead()) {
-            LOG.error("File not readable: " + file.getAbsolutePath());
-            return new ResponseEntity<>("File " + file.getName() + " is not readable", INTERNAL_SERVER_ERROR);
-        }
-
-        String vosName = fileInfo.getVirtualPath().substring(fileInfo.getVirtualPath().lastIndexOf("/") + 1);
-
-        response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(vosName, StandardCharsets.UTF_8));
-        response.setHeader("Content-Length", String.valueOf(file.length()));
-        response.setCharacterEncoding("UTF-8");
-
-        byte[] bytes = new byte[1024];
-        try (OutputStream out = response.getOutputStream();
-                InputStream is = new FileInputStream(file)) {
-            int read;
-            while ((read = is.read(bytes)) != -1) {
-                out.write(bytes, 0, read);
-            }
-        } catch (IOException ex) {
-            throw new UncheckedIOException(ex);
-        }
-        return null;
+        return FileResponseUtil.getFileResponse(response, file, vosName);
     }
 }
diff --git a/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java b/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java
index 9f5aa85ecd98d560dd60b65278b88c54521567fc..9ead6d5fc9cf90fae8b79bce086e72bb1b134e24 100644
--- a/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java
+++ b/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java
@@ -128,22 +128,22 @@ public class FileDAO {
             throw new IllegalArgumentException("Received empty list of paths");
         }
 
-        String sql = "SELECT n.node_id, is_public, group_read, group_write, creator_id, async_trans,\n"
-                + "content_type, content_encoding, content_length, content_md5,\n"
-                + "accept_views, provide_views, l.location_type, n.path <> n.relative_path AS virtual_parent,\n"
-                + "(SELECT user_name FROM users WHERE user_id = creator_id) AS username,\n"
+        String sql = "SELECT n.node_id, n.is_public, n.group_read, n.group_write, n.creator_id, n.async_trans,\n"
+                + "n.content_type, n.content_encoding, n.content_length, n.content_md5,\n"
+                + "n.accept_views, n.provide_views, l.location_type, n.path <> n.relative_path AS virtual_parent,\n"
+                + "(SELECT user_name FROM users WHERE user_id = n.creator_id) AS username,\n"
                 + "base_path, get_os_path(n.node_id) AS os_path, get_vos_path(n.node_id) AS vos_path,\n"
-                + "type = 'container' AS is_directory\n"
+                + "n.type = 'container' AS is_directory\n"
                 + "FROM node n\n"
-                + "JOIN location l ON (n.location_id IS NOT NULL AND n.location_id = l.location_id) OR (n.location_id IS NULL AND l.location_id = ?)\n"
+                + "JOIN node p ON p.path @> n.path\n"
+                + "LEFT JOIN location l ON l.location_id = n.location_id\n"
                 + "LEFT JOIN storage s ON s.storage_id = l.storage_dest_id\n"
-                + "WHERE " + String.join(" OR ", Collections.nCopies(vosPaths.size(), "n.node_id  = id_from_vos_path(?)"))
+                + "WHERE " + String.join(" OR ", Collections.nCopies(vosPaths.size(), "p.node_id  = id_from_vos_path(?)"))
                 + "\nORDER BY vos_path ASC";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
             int i = 0;
-            ps.setInt(++i, uploadLocationId);
             for (String vosPath : vosPaths) {
                 ps.setString(++i, vosPath);
             }
@@ -183,6 +183,10 @@ public class FileDAO {
     private void fillOsPath(FileInfo fi, ResultSet rs) throws SQLException {
 
         String basePath = rs.getString("base_path");
+        if (basePath == null) {
+            return;
+        }
+
         String osPath = rs.getString("os_path");
         if (osPath.startsWith("/")) {
             osPath = osPath.substring(1);
diff --git a/src/main/java/it/inaf/ia2/transfer/service/ArchiveJob.java b/src/main/java/it/inaf/ia2/transfer/service/ArchiveJob.java
index 0ae2c083e242723d11b7369455b3927d6fda546d..631660085f11033926039626bde1c2c551807c4a 100644
--- a/src/main/java/it/inaf/ia2/transfer/service/ArchiveJob.java
+++ b/src/main/java/it/inaf/ia2/transfer/service/ArchiveJob.java
@@ -12,7 +12,21 @@ public class ArchiveJob {
 
     public static enum Type {
         TAR,
-        ZIP
+        TGZ,
+        ZIP;
+
+        public String getExtension() {
+            switch (this) {
+                case TAR:
+                    return "tar";
+                case TGZ:
+                    return "tar.gz";
+                case ZIP:
+                    return "zip";
+                default:
+                    throw new IllegalArgumentException("Extension not defined for type " + this);
+            }
+        }
     }
 
     private List<String> vosPaths;
diff --git a/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java b/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java
index b71db9faf32710de8b2a0c536911272ccc8e2114..11b3cef0680138db9ad30529015f8ad4c82859c3 100644
--- a/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java
+++ b/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java
@@ -15,6 +15,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
+import java.security.Principal;
 import java.util.List;
 import org.kamranzafar.jtar.TarEntry;
 import org.kamranzafar.jtar.TarOutputStream;
@@ -56,7 +57,7 @@ public class ArchiveService {
         try {
             // TODO: check total size limit
             // TODO: switch on archive type
-            File parentDir = generatedDir.toPath().resolve(job.getPrincipal().getName()).toFile();
+            File parentDir = getArchiveParentDir(job.getPrincipal());
 
             if (!parentDir.exists()) {
                 if (!parentDir.mkdirs()) {
@@ -64,7 +65,7 @@ public class ArchiveService {
                 }
             }
 
-            File archiveFile = parentDir.toPath().resolve(job.getJobId() + ".tar").toFile();
+            File archiveFile = parentDir.toPath().resolve(job.getJobId() + "." + job.getType().getExtension()).toFile();
             if (!archiveFile.createNewFile()) {
                 throw new IllegalStateException("Unable to create file " + archiveFile.getAbsolutePath());
             }
@@ -105,6 +106,10 @@ public class ArchiveService {
         }
     }
 
+    public File getArchiveParentDir(Principal principal) {
+        return generatedDir.toPath().resolve(principal.getName()).toFile();
+    }
+
     private String getCommonParent(List<String> vosPaths) {
         String commonParent = null;
         for (String vosPath : vosPaths) {
diff --git a/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java b/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java
index 08477b992a84d346b278f487b07d56059415a014..e353fdf61100e57d6b2820d74a68a6ab20d391b9 100644
--- a/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java
+++ b/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java
@@ -5,8 +5,10 @@
  */
 package it.inaf.ia2.transfer.controller;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import it.inaf.ia2.transfer.service.ArchiveJob;
 import it.inaf.ia2.transfer.service.ArchiveService;
+import java.util.Arrays;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import org.junit.jupiter.api.Test;
 import static org.mockito.ArgumentMatchers.argThat;
@@ -26,6 +28,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 @AutoConfigureMockMvc
 public class ArchiveFileControllerTest {
 
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
     @MockBean
     private ArchiveService archiveService;
 
@@ -35,11 +39,16 @@ public class ArchiveFileControllerTest {
     @Test
     public void testCreateTarArchive() throws Exception {
 
-        mockMvc.perform(post("/tar?jobId=123")
+        ArchiveRequest request = new ArchiveRequest();
+        request.setJobId("123");
+        request.setType("TAR");
+        request.setPaths(Arrays.asList("/path/to/file1", "/path/to/file2"));
+
+        mockMvc.perform(post("/archive")
                 .contentType(MediaType.APPLICATION_JSON)
-                .content("[\"/path/to/file1\", \"/path/to/file2\"]"))
+                .content(MAPPER.writeValueAsString(request)))
                 .andDo(print())
-                .andExpect(status().isOk());
+                .andExpect(status().is3xxRedirection());
 
         verify(archiveService, times(1)).createArchive(argThat(job -> {
             assertEquals("123", job.getJobId());
diff --git a/src/test/java/it/inaf/ia2/transfer/persistence/FileDAOTest.java b/src/test/java/it/inaf/ia2/transfer/persistence/FileDAOTest.java
index 76e69f38842316c6fabb676fb5e93d8fb13c9f8f..d82e3d940cbe2ab25e9fbe5758f75be14c9dca7c 100644
--- a/src/test/java/it/inaf/ia2/transfer/persistence/FileDAOTest.java
+++ b/src/test/java/it/inaf/ia2/transfer/persistence/FileDAOTest.java
@@ -6,6 +6,8 @@
 package it.inaf.ia2.transfer.persistence;
 
 import it.inaf.ia2.transfer.persistence.model.FileInfo;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Optional;
 import javax.sql.DataSource;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -44,4 +46,18 @@ public class FileDAOTest {
 
         assertEquals("/home/username1/retrieve/file1.txt", fileInfo.getOsPath());
     }
+
+    @Test
+    public void testGetArchiveFileInfos() {
+
+        List<FileInfo> fileInfos = dao.getArchiveFileInfos(Arrays.asList("/public/file1", "/public/file2", "/public/subdir1"));
+
+        assertEquals(5, fileInfos.size());
+
+        assertEquals("/home/vospace/upload/user1/file1", fileInfos.get(0).getOsPath());
+        assertEquals("/home/vospace/upload/user1/file2", fileInfos.get(1).getOsPath());
+        assertTrue(fileInfos.get(2).isDirectory());
+        assertEquals("/home/username1/retrieve/subdir1/file3", fileInfos.get(3).getOsPath());
+        assertEquals("/home/username1/retrieve/subdir1/file4", fileInfos.get(4).getOsPath());
+    }
 }
diff --git a/src/test/resources/test-data.sql b/src/test/resources/test-data.sql
index 5bff95d41262515e96c3685824b818dd66ff9d7c..0e719b5c9fa9884790970714595e9d9c504c4a89 100644
--- a/src/test/resources/test-data.sql
+++ b/src/test/resources/test-data.sql
@@ -18,10 +18,18 @@ INSERT INTO users (user_id, user_name, e_mail) VALUES ('user1', 'username1', 'ia
 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id) VALUES (NULL, NULL, '', 'container', '0');
 
 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_read, group_write) VALUES ('', NULL, 'test1', 'container', 'user1', '{"group1","group2"}','{"group2"}');      -- /test1
-INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_read, group_write) VALUES ('2', NULL, '.tmp-123.txt', 'structured', 'user1', '{"group1","group2"}','{"group2"}');      -- /test1/.tmp-123.txt
 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_read, group_write, location_id) VALUES ('2', '', 'file1.txt', 'data', 'user1', '{"group1","group2"}','{"group2"}', 1);      -- /test1/file1.txt
 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_read, group_write, location_id) VALUES ('2', '', 'file2.txt', 'data', 'user1', '{"group1","group2"}','{"group2"}', 1);      -- /test1/file2.txt
 
+-- test data for tar/zip archive
+INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, location_id, is_public) VALUES
+('', NULL, 'public', 'container', 'user1', NULL, true),
+('5', '', 'file1', 'data', 'user1', 3, true),
+('5', '', 'file2', 'data', 'user1', 3, true),
+('5', '', 'subdir1', 'container', 'user1', NULL, true),
+('5.8', '8', 'file3', 'data', 'user1', 1, true),
+('5.8', '8', 'file4', 'data', 'user1', 1, true);
+
 DELETE FROM job;
 
 INSERT INTO job (job_id, owner_id, job_type, phase, start_time, end_time, creation_time, job_info, results) VALUES ('pippo1', 'user1', 'pullFromVoSpace', 'ARCHIVED', NULL, NULL, '2011-06-22 19:10:25', NULL, NULL);