diff --git a/pom.xml b/pom.xml
index 994cdaaa6de2e8e80853e11e61ac8543a30555fe..59a899333b52ff54c7a50344e2ca10c0e640dc31 100644
--- a/pom.xml
+++ b/pom.xml
@@ -72,6 +72,12 @@
             <artifactId>vospace-datamodel</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>
+        
+        <dependency>
+            <groupId>org.kamranzafar</groupId>
+            <artifactId>jtar</artifactId>
+            <version>2.3</version>
+        </dependency>
     </dependencies>
 
     <profiles>
diff --git a/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java
new file mode 100644
index 0000000000000000000000000000000000000000..7adc30cd3d4d1b4382f7b787df12b5d75a4a23cb
--- /dev/null
+++ b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java
@@ -0,0 +1,47 @@
+/*
+ * 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 it.inaf.ia2.transfer.auth.TokenPrincipal;
+import it.inaf.ia2.transfer.service.ArchiveJob;
+import it.inaf.ia2.transfer.service.ArchiveService;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import javax.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+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
+public class ArchiveFileController {
+
+    @Autowired
+    private ArchiveService archiveService;
+
+    @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) {
+
+        ArchiveJob job = new ArchiveJob();
+        job.setPrincipal((TokenPrincipal) request.getUserPrincipal());
+        job.setJobId(jobId);
+        job.setType(ArchiveJob.Type.TAR);
+        job.setVosPaths(vosPaths);
+
+        startArchiveJob(job);
+    }
+
+    private void startArchiveJob(ArchiveJob job) {
+        CompletableFuture.runAsync(() -> {
+            archiveService.createArchive(job);
+        });
+    }
+}
diff --git a/src/main/java/it/inaf/ia2/transfer/controller/ArchiveRequest.java b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveRequest.java
new file mode 100644
index 0000000000000000000000000000000000000000..91cef3448b7dcc30ac5dd4f049a936232f5cc63c
--- /dev/null
+++ b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveRequest.java
@@ -0,0 +1,16 @@
+/*
+ * 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.util.List;
+
+public class ArchiveRequest {
+
+    private List<String> vosPaths;
+    String jobId;
+    String type;
+}
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 cab9e3a0441d4b54ec772dde1b9f94c87f103cd9..be4e4fe28d8f1adcd206f386bf6adaa72a10abf1 100644
--- a/src/main/java/it/inaf/ia2/transfer/controller/GetFileController.java
+++ b/src/main/java/it/inaf/ia2/transfer/controller/GetFileController.java
@@ -5,9 +5,10 @@
  */
 package it.inaf.ia2.transfer.controller;
 
-import it.inaf.ia2.transfer.auth.GmsClient;
+import it.inaf.ia2.transfer.persistence.model.FileInfo;
 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;
@@ -37,7 +38,7 @@ public class GetFileController extends FileController {
     private FileDAO fileDAO;
 
     @Autowired
-    private GmsClient gmsClient;
+    private AuthorizationService authorizationService;
 
     @Autowired
     private HttpServletResponse response;
@@ -46,16 +47,16 @@ public class GetFileController extends FileController {
     public ResponseEntity<?> getFile() {
 
         String path = getPath();
-        
+
         LOG.debug("getFile called for path {}", path);
-        
+
         Optional<FileInfo> optFileInfo = fileDAO.getFileInfo(path);
 
         if (optFileInfo.isPresent()) {
 
             FileInfo fileInfo = optFileInfo.get();
 
-            if (!fileInfo.isIsPublic() && !privateButDownloadable(fileInfo)) {
+            if (!authorizationService.isDownloadable(fileInfo, (TokenPrincipal) request.getUserPrincipal())) {
                 return new ResponseEntity<>("Unauthorized", UNAUTHORIZED);
             }
 
@@ -65,31 +66,6 @@ public class GetFileController extends FileController {
         }
     }
 
-    private boolean privateButDownloadable(FileInfo fileInfo) {
-
-        TokenPrincipal principal = (TokenPrincipal) request.getUserPrincipal();
-
-        String token = principal.getToken();
-        if (token == null) {
-            return false;
-        }
-
-        if (principal.getName().equals(fileInfo.getOwnerId())) {
-            return true;
-        }
-
-        // TODO: configure cache
-        if (fileInfo.getGroupRead() == null) {
-            return false;
-        }
-        for (String group : fileInfo.getGroupRead()) {
-            if (gmsClient.isMemberOf(token, group)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     private ResponseEntity<?> getFileResponse(FileInfo fileInfo) {
 
         File file = new File(fileInfo.getOsPath());
@@ -105,7 +81,7 @@ public class GetFileController extends FileController {
         }
 
         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");
diff --git a/src/main/java/it/inaf/ia2/transfer/controller/PutFileController.java b/src/main/java/it/inaf/ia2/transfer/controller/PutFileController.java
index e9c8e7f4e60cc5e1c7ec94cbeabe07be52b17203..5bdbae00166c01bccd1ddaa66234b51fd76599da 100644
--- a/src/main/java/it/inaf/ia2/transfer/controller/PutFileController.java
+++ b/src/main/java/it/inaf/ia2/transfer/controller/PutFileController.java
@@ -5,6 +5,7 @@
  */
 package it.inaf.ia2.transfer.controller;
 
+import it.inaf.ia2.transfer.persistence.model.FileInfo;
 import it.inaf.ia2.transfer.persistence.FileDAO;
 import it.inaf.ia2.transfer.persistence.JobDAO;
 import java.io.File;
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 ce9c335de2fb5c397ad2c78cd41dd9d753101e7e..6a22024875a87de8a5c984f68993096162d6429e 100644
--- a/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java
+++ b/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java
@@ -5,7 +5,7 @@
  */
 package it.inaf.ia2.transfer.persistence;
 
-import it.inaf.ia2.transfer.controller.FileInfo;
+import it.inaf.ia2.transfer.persistence.model.FileInfo;
 import java.nio.file.Path;
 import java.sql.Array;
 import java.sql.PreparedStatement;
@@ -14,6 +14,7 @@ import java.sql.SQLException;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import javax.sql.DataSource;
@@ -41,7 +42,7 @@ public class FileDAO {
                 + "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"
-                + "base_path, get_os_path(n.node_id) AS os_path\n"
+                + "base_path, get_os_path(n.node_id) AS os_path, ? AS vos_path\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"
                 + "LEFT JOIN storage s ON s.storage_id = l.storage_dest_id\n"
@@ -49,30 +50,13 @@ public class FileDAO {
 
         FileInfo fileInfo = jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
-            ps.setInt(1, uploadLocationId);
-            ps.setString(2, virtualPath);
+            ps.setString(1, virtualPath);
+            ps.setInt(2, uploadLocationId);
+            ps.setString(3, virtualPath);
             return ps;
         }, rs -> {
             if (rs.next()) {
-                FileInfo fi = new FileInfo();
-                fi.setNodeId(rs.getInt("node_id"));
-                fi.setIsPublic(rs.getBoolean("is_public"));
-                fi.setGroupRead(toList(rs.getArray("group_read")));
-                fi.setGroupWrite(toList(rs.getArray("group_write")));
-                fi.setOwnerId(rs.getString("creator_id"));
-                fi.setAsyncTrans(rs.getBoolean("async_trans"));
-                fi.setAcceptViews(toList(rs.getArray("accept_views")));
-                fi.setProvideViews(toList(rs.getArray("provide_views")));
-                fi.setVirtualParent(rs.getBoolean("virtual_parent"));
-                fi.setVirtualPath(virtualPath);
-                fi.setContentEncoding(rs.getString("content_encoding"));
-                fi.setContentLength(rs.getLong("content_length"));
-                fi.setContentMd5(rs.getString("content_md5"));
-                fi.setContentType(rs.getString("content_type"));
-
-                fillOsPath(fi, rs);
-
-                return fi;
+                return getFileInfo(rs);
             }
             return null;
         });
@@ -80,38 +64,6 @@ public class FileDAO {
         return Optional.ofNullable(fileInfo);
     }
 
-    private void fillOsPath(FileInfo fi, ResultSet rs) throws SQLException {
-
-        if (fi.getProvideViews() != null) {
-            for (String provideView : fi.getProvideViews()) {
-                if ("urn:list-of-files".equals(provideView)) {
-                    // Not a physical file
-                    return;
-                }
-            }
-        }
-
-        String basePath = rs.getString("base_path");
-        String osPath = rs.getString("os_path");
-        if (osPath.startsWith("/")) {
-            osPath = osPath.substring(1);
-        }
-
-        Path completeOsPath = Path.of(basePath);
-
-        boolean asyncLocation = "async".equals(rs.getString("location_type"));
-
-        if (asyncLocation) {
-            String username = rs.getString("username");
-            completeOsPath = completeOsPath.resolve(username).resolve("retrieve");
-        } else if (fi.hasVirtualParent()) {
-            completeOsPath = completeOsPath.resolve(fi.getOwnerId());
-        }
-
-        completeOsPath = completeOsPath.resolve(osPath);
-        fi.setOsPath(completeOsPath.toString());
-    }
-
     private List<String> toList(Array array) throws SQLException {
         if (array == null) {
             return new ArrayList<>();
@@ -168,4 +120,84 @@ public class FileDAO {
         });
 
     }
+
+    // TODO: set maximum list size or use stream to avoid memory issues
+    public List<FileInfo> getArchiveFileInfos(List<String> vosPaths) {
+
+        if (vosPaths.isEmpty()) {
+            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"
+                + "base_path, get_os_path(n.node_id) AS os_path, get_vos_path(n.node_id) AS vos_path\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"
+                + "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(?)"))
+                + "\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);
+            }
+            return ps;
+        }, rs -> {
+            List<FileInfo> fileInfos = new ArrayList<>();
+            while (rs.next()) {
+                fileInfos.add(getFileInfo(rs));
+            }
+            return fileInfos;
+        });
+    }
+
+    private FileInfo getFileInfo(ResultSet rs) throws SQLException {
+        FileInfo fi = new FileInfo();
+        fi.setNodeId(rs.getInt("node_id"));
+        fi.setPublic(rs.getBoolean("is_public"));
+        fi.setGroupRead(toList(rs.getArray("group_read")));
+        fi.setGroupWrite(toList(rs.getArray("group_write")));
+        fi.setOwnerId(rs.getString("creator_id"));
+        fi.setAsyncTrans(rs.getBoolean("async_trans"));
+        fi.setAcceptViews(toList(rs.getArray("accept_views")));
+        fi.setProvideViews(toList(rs.getArray("provide_views")));
+        fi.setVirtualParent(rs.getBoolean("virtual_parent"));
+        fi.setVirtualPath(rs.getString("vos_path"));
+        fi.setContentEncoding(rs.getString("content_encoding"));
+        fi.setContentLength(rs.getLong("content_length"));
+        fi.setContentMd5(rs.getString("content_md5"));
+        fi.setContentType(rs.getString("content_type"));
+
+        fillOsPath(fi, rs);
+
+        return fi;
+    }
+
+    private void fillOsPath(FileInfo fi, ResultSet rs) throws SQLException {
+
+        String basePath = rs.getString("base_path");
+        String osPath = rs.getString("os_path");
+        if (osPath.startsWith("/")) {
+            osPath = osPath.substring(1);
+        }
+
+        Path completeOsPath = Path.of(basePath);
+
+        boolean asyncLocation = "async".equals(rs.getString("location_type"));
+
+        if (asyncLocation) {
+            String username = rs.getString("username");
+            completeOsPath = completeOsPath.resolve(username).resolve("retrieve");
+        } else if (fi.hasVirtualParent()) {
+            completeOsPath = completeOsPath.resolve(fi.getOwnerId());
+        }
+
+        completeOsPath = completeOsPath.resolve(osPath);
+        fi.setOsPath(completeOsPath.toString());
+    }
 }
diff --git a/src/main/java/it/inaf/ia2/transfer/controller/FileInfo.java b/src/main/java/it/inaf/ia2/transfer/persistence/model/FileInfo.java
similarity index 96%
rename from src/main/java/it/inaf/ia2/transfer/controller/FileInfo.java
rename to src/main/java/it/inaf/ia2/transfer/persistence/model/FileInfo.java
index 9a0c717f78eec7b39060c52a87b2e5841d3181be..3fb8d09612ec34c8bc6f4140b8d1d1eb74cec3b5 100644
--- a/src/main/java/it/inaf/ia2/transfer/controller/FileInfo.java
+++ b/src/main/java/it/inaf/ia2/transfer/persistence/model/FileInfo.java
@@ -3,7 +3,7 @@
  * Copyright (C) 2021 Istituto Nazionale di Astrofisica
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
-package it.inaf.ia2.transfer.controller;
+package it.inaf.ia2.transfer.persistence.model;
 
 import java.util.List;
 
@@ -81,11 +81,11 @@ public class FileInfo {
         this.virtualPath = virtualPath;
     }
 
-    public boolean isIsPublic() {
+    public boolean isPublic() {
         return isPublic;
     }
 
-    public void setIsPublic(boolean isPublic) {
+    public void setPublic(boolean isPublic) {
         this.isPublic = isPublic;
     }
 
diff --git a/src/main/java/it/inaf/ia2/transfer/service/ArchiveJob.java b/src/main/java/it/inaf/ia2/transfer/service/ArchiveJob.java
new file mode 100644
index 0000000000000000000000000000000000000000..0ae2c083e242723d11b7369455b3927d6fda546d
--- /dev/null
+++ b/src/main/java/it/inaf/ia2/transfer/service/ArchiveJob.java
@@ -0,0 +1,54 @@
+/*
+ * 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.service;
+
+import it.inaf.ia2.transfer.auth.TokenPrincipal;
+import java.util.List;
+
+public class ArchiveJob {
+
+    public static enum Type {
+        TAR,
+        ZIP
+    }
+
+    private List<String> vosPaths;
+    private TokenPrincipal tokenPrincipal;
+    private String jobId;
+    private Type type;
+
+    public List<String> getVosPaths() {
+        return vosPaths;
+    }
+
+    public void setVosPaths(List<String> vosPaths) {
+        this.vosPaths = vosPaths;
+    }
+
+    public TokenPrincipal getPrincipal() {
+        return tokenPrincipal;
+    }
+
+    public void setPrincipal(TokenPrincipal tokenPrincipal) {
+        this.tokenPrincipal = tokenPrincipal;
+    }
+
+    public String getJobId() {
+        return jobId;
+    }
+
+    public void setJobId(String jobId) {
+        this.jobId = jobId;
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    public void setType(Type type) {
+        this.type = type;
+    }
+}
diff --git a/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java b/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java
new file mode 100644
index 0000000000000000000000000000000000000000..8ff8c96a78cea2e23681ed160db4d294ade32f42
--- /dev/null
+++ b/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java
@@ -0,0 +1,109 @@
+/*
+ * 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.service;
+
+import it.inaf.ia2.transfer.persistence.FileDAO;
+import it.inaf.ia2.transfer.persistence.model.FileInfo;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import org.kamranzafar.jtar.TarEntry;
+import org.kamranzafar.jtar.TarOutputStream;
+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;
+
+@Service
+public class ArchiveService {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ArchiveService.class);
+
+    private final static int BUFFER_SIZE = 100 * 1024;
+
+    @Autowired
+    private FileDAO fileDAO;
+
+    @Autowired
+    private AuthorizationService authorizationService;
+
+    private final File generatedDir;
+
+    public ArchiveService(@Value("${generated.dir}") String generatedDir) {
+        this.generatedDir = new File(generatedDir);
+        if (!this.generatedDir.exists()) {
+            if (!this.generatedDir.mkdirs()) {
+                throw new IllegalStateException("Unable to create directory " + this.generatedDir.getAbsolutePath());
+            }
+        }
+    }
+
+    public void createArchive(ArchiveJob job) {
+
+        LOG.trace("Started archive job " + job.getJobId());
+
+        try {
+            // TODO: check total size limit
+            // TODO: switch on archive type
+            File parentDir = generatedDir.toPath().resolve(job.getPrincipal().getName()).toFile();
+
+            if (!parentDir.exists()) {
+                if (!parentDir.mkdirs()) {
+                    throw new IllegalStateException("Unable to create directory " + parentDir.getAbsolutePath());
+                }
+            }
+
+            File archiveFile = parentDir.toPath().resolve(job.getJobId() + ".tar").toFile();
+            if (!archiveFile.createNewFile()) {
+                throw new IllegalStateException("Unable to create file " + archiveFile.getAbsolutePath());
+            }
+
+            try ( TarOutputStream tos = new TarOutputStream(
+                    new BufferedOutputStream(new FileOutputStream(archiveFile)))) {
+
+                for (FileInfo fileInfo : fileDAO.getArchiveFileInfos(job.getVosPaths())) {
+                    // TODO: handle different locations
+
+                    if (!authorizationService.isDownloadable(fileInfo, job.getPrincipal())) {
+                        // TODO: proper exception type
+                        throw new RuntimeException("Unauthorized");
+                    }
+
+                    File file = new File(fileInfo.getOsPath());
+                    LOG.trace("Adding file " + file.getAbsolutePath() + " to tar archive");
+                    writeFileIntoTarArchive(file, tos);
+                }
+            }
+            // TODO: update job status
+
+        } catch (Throwable t) {
+            LOG.error("Error happened creating archive", t);
+        }
+    }
+
+    private void writeFileIntoTarArchive(File file, TarOutputStream tos) throws IOException {
+        TarEntry tarEntry = new TarEntry(file, file.getName());
+
+        try ( InputStream is = new FileInputStream(file)) {
+            tos.putNextEntry(tarEntry);
+            try ( BufferedInputStream origin = new BufferedInputStream(is)) {
+                int count;
+                byte data[] = new byte[BUFFER_SIZE];
+
+                while ((count = origin.read(data)) != -1) {
+                    tos.write(data, 0, count);
+                }
+
+                tos.flush();
+            }
+        }
+    }
+}
diff --git a/src/main/java/it/inaf/ia2/transfer/service/AuthorizationService.java b/src/main/java/it/inaf/ia2/transfer/service/AuthorizationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e40fc50643171410d1fdf529d711524bc6c941c
--- /dev/null
+++ b/src/main/java/it/inaf/ia2/transfer/service/AuthorizationService.java
@@ -0,0 +1,46 @@
+/*
+ * 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.service;
+
+import it.inaf.ia2.transfer.auth.GmsClient;
+import it.inaf.ia2.transfer.auth.TokenPrincipal;
+import it.inaf.ia2.transfer.persistence.model.FileInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AuthorizationService {
+
+    @Autowired
+    private GmsClient gmsClient;
+
+    public boolean isDownloadable(FileInfo fileInfo, TokenPrincipal principal) {
+
+        if (fileInfo.isPublic()) {
+            return true;
+        }
+
+        String token = principal.getToken();
+        if (token == null) {
+            return false;
+        }
+
+        if (principal.getName().equals(fileInfo.getOwnerId())) {
+            return true;
+        }
+
+        // TODO: configure cache
+        if (fileInfo.getGroupRead() == null) {
+            return false;
+        }
+        for (String group : fileInfo.getGroupRead()) {
+            if (gmsClient.isMemberOf(token, group)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 5379521217b6dbf94d5491f3e0575b08403aa041..1dca7a0293234ff3aa3b8092a6b1270e78b0568d 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -5,8 +5,7 @@ file-catalog.datasource.jdbc-url=jdbc:postgresql://127.0.0.1:5432/vospace_testdb
 file-catalog.datasource.username=postgres
 file-catalog.datasource.password=
 
-gms_base_url=http://localhost:8082/gms
-jwks_uri=http://localhost/rap-ia2/auth/oidc/jwks
+generated.dir=/tmp/vospace/gen
 
 upload_location_id=3
 
diff --git a/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java b/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..08477b992a84d346b278f487b07d56059415a014
--- /dev/null
+++ b/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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 it.inaf.ia2.transfer.service.ArchiveJob;
+import it.inaf.ia2.transfer.service.ArchiveService;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+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.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+public class ArchiveFileControllerTest {
+
+    @MockBean
+    private ArchiveService archiveService;
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Test
+    public void testCreateTarArchive() throws Exception {
+
+        mockMvc.perform(post("/tar?jobId=123")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content("[\"/path/to/file1\", \"/path/to/file2\"]"))
+                .andDo(print())
+                .andExpect(status().isOk());
+
+        verify(archiveService, times(1)).createArchive(argThat(job -> {
+            assertEquals("123", job.getJobId());
+            assertEquals(ArchiveJob.Type.TAR, job.getType());
+            assertEquals("anonymous", job.getPrincipal().getName());
+            assertEquals(2, job.getVosPaths().size());
+            return true;
+        }));
+    }
+}
diff --git a/src/test/java/it/inaf/ia2/transfer/controller/GetFileControllerTest.java b/src/test/java/it/inaf/ia2/transfer/controller/GetFileControllerTest.java
index d90d0f3a0865f472278cbe9d61b4b6b970cf937d..d67b07a17a775feecf7a138f0a4dd5246000e6c4 100644
--- a/src/test/java/it/inaf/ia2/transfer/controller/GetFileControllerTest.java
+++ b/src/test/java/it/inaf/ia2/transfer/controller/GetFileControllerTest.java
@@ -5,6 +5,7 @@
  */
 package it.inaf.ia2.transfer.controller;
 
+import it.inaf.ia2.transfer.persistence.model.FileInfo;
 import it.inaf.ia2.aa.jwt.TokenParser;
 import it.inaf.ia2.transfer.auth.GmsClient;
 import it.inaf.ia2.transfer.persistence.FileDAO;
@@ -64,7 +65,7 @@ public class GetFileControllerTest {
         FileInfo fileInfo = new FileInfo();
         fileInfo.setOsPath(tempFile.getAbsolutePath());
         fileInfo.setVirtualPath("/path/to/myfile");
-        fileInfo.setIsPublic(true);
+        fileInfo.setPublic(true);
 
         when(fileDao.getFileInfo(eq("/path/to/myfile"))).thenReturn(Optional.of(fileInfo));
 
@@ -79,7 +80,7 @@ public class GetFileControllerTest {
         FileInfo fileInfo = new FileInfo();
         fileInfo.setOsPath(tempFile.getAbsolutePath());
         fileInfo.setVirtualPath("/path/to/myfile");
-        fileInfo.setIsPublic(true);
+        fileInfo.setPublic(true);
 
         when(fileDao.getFileInfo(eq("/path/to/myfile"))).thenReturn(Optional.of(fileInfo));
 
@@ -100,7 +101,7 @@ public class GetFileControllerTest {
 
         FileInfo fileInfo = new FileInfo();
         fileInfo.setOsPath("/this/doesnt/exists");
-        fileInfo.setIsPublic(true);
+        fileInfo.setPublic(true);
 
         when(fileDao.getFileInfo(any())).thenReturn(Optional.of(fileInfo));
 
diff --git a/src/test/java/it/inaf/ia2/transfer/controller/PutFileControllerTest.java b/src/test/java/it/inaf/ia2/transfer/controller/PutFileControllerTest.java
index 33909c9f9708ef30d2dff3b918e6aab1c86dde85..e2b84b1f358b323fe2cd94727904fdd146327a43 100644
--- a/src/test/java/it/inaf/ia2/transfer/controller/PutFileControllerTest.java
+++ b/src/test/java/it/inaf/ia2/transfer/controller/PutFileControllerTest.java
@@ -5,6 +5,7 @@
  */
 package it.inaf.ia2.transfer.controller;
 
+import it.inaf.ia2.transfer.persistence.model.FileInfo;
 import it.inaf.ia2.transfer.persistence.FileDAO;
 import it.inaf.ia2.transfer.persistence.JobDAO;
 import java.io.File;
@@ -174,7 +175,7 @@ public class PutFileControllerTest {
     private FileInfo createBaseFileInfo(String fileName) {
         FileInfo fileInfo = new FileInfo();
         fileInfo.setOsPath(getTestFilePath(fileName));
-        fileInfo.setIsPublic(false);
+        fileInfo.setPublic(false);
 
         when(fileDao.getFileInfo(any())).thenReturn(Optional.of(fileInfo));
 
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 77a23d82b377bcf9a0eb34e1ebb2a6ebc2130b61..76e69f38842316c6fabb676fb5e93d8fb13c9f8f 100644
--- a/src/test/java/it/inaf/ia2/transfer/persistence/FileDAOTest.java
+++ b/src/test/java/it/inaf/ia2/transfer/persistence/FileDAOTest.java
@@ -5,7 +5,7 @@
  */
 package it.inaf.ia2.transfer.persistence;
 
-import it.inaf.ia2.transfer.controller.FileInfo;
+import it.inaf.ia2.transfer.persistence.model.FileInfo;
 import java.util.Optional;
 import javax.sql.DataSource;
 import static org.junit.jupiter.api.Assertions.assertEquals;
diff --git a/src/test/java/it/inaf/ia2/transfer/service/ArchiveServiceTest.java b/src/test/java/it/inaf/ia2/transfer/service/ArchiveServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..09aadff229459d8b3b019dedaed7b72245232d1f
--- /dev/null
+++ b/src/test/java/it/inaf/ia2/transfer/service/ArchiveServiceTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.service;
+
+import it.inaf.ia2.transfer.auth.TokenPrincipal;
+import it.inaf.ia2.transfer.persistence.FileDAO;
+import it.inaf.ia2.transfer.persistence.model.FileInfo;
+import java.io.File;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.AfterAll;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import org.mockito.Mock;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.util.FileSystemUtils;
+
+@ExtendWith(MockitoExtension.class)
+public class ArchiveServiceTest {
+
+    @Mock
+    private FileDAO fileDAO;
+
+    @Mock
+    private AuthorizationService authorizationService;
+
+    private ArchiveService archiveService;
+
+    private static File tmpDir;
+
+    @BeforeAll
+    public static void setUpClass() throws Exception {
+        tmpDir = Files.createTempDirectory("generated").toFile();
+    }
+
+    @AfterAll
+    public static void tearDownClass() throws Exception {
+        FileSystemUtils.deleteRecursively(tmpDir);
+    }
+
+    @BeforeEach
+    public void setUp() {
+        archiveService = new ArchiveService(tmpDir.getAbsolutePath());
+        ReflectionTestUtils.setField(archiveService, "fileDAO", fileDAO);
+        ReflectionTestUtils.setField(archiveService, "authorizationService", authorizationService);
+    }
+
+    @Test
+    public void testTarGeneration() throws Exception {
+
+        File tmpParent = tmpDir.toPath().resolve("test1").toFile();
+        File file1 = createFile(tmpParent, "dir1/a/b/file1");
+        File file2 = createFile(tmpParent, "dir1/a/b/file2");
+        File file3 = createFile(tmpParent, "dir2/c/file3");
+        File file4 = createFile(tmpParent, "dir2/c/file4");
+        File file5 = createFile(tmpParent, "dir2/c/d/file5");
+        File file6 = createFile(tmpParent, "file6");
+
+        ArchiveJob job = new ArchiveJob();
+        job.setPrincipal(new TokenPrincipal("user123", "token123"));
+        job.setJobId("abcdef");
+        job.setType(ArchiveJob.Type.TAR);
+        job.setVosPaths(Arrays.asList("/path/to/file6"));//"/path/to/dir1", "/path/to/dir2",
+
+        when(authorizationService.isDownloadable(any(), any())).thenReturn(true);
+
+        List<FileInfo> fileInfos = new ArrayList<>();
+        FileInfo fileInfo = new FileInfo();
+        fileInfo.setOsPath(file6.getAbsolutePath());
+        fileInfos.add(fileInfo);
+
+        when(fileDAO.getArchiveFileInfos(any())).thenReturn(fileInfos);
+
+        archiveService.createArchive(job);
+
+        File result = tmpDir.toPath().resolve("user123").resolve("abcdef.tar").toFile();
+        assertTrue(result.exists());
+    }
+
+    private File createFile(File parent, String path) throws Exception {
+        parent.mkdir();
+        String[] files = path.split("/");
+        for (int i = 0; i < files.length; i++) {
+            File file = parent.toPath().resolve(files[i]).toFile();
+            if (i == files.length - 1) {
+                file.createNewFile();
+                Files.write(file.toPath(), "some data".getBytes());
+                return file;
+            } else {
+                file.mkdir();
+                parent = file;
+            }
+        }
+        throw new IllegalStateException("Files have to be created");
+    }
+}