Newer
Older
/*
* 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.aa.ServletRapClient;
import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.rap.client.call.TokenExchangeRequest;
Sonia Zorba
committed
import it.inaf.ia2.transfer.auth.TokenPrincipal;
import it.inaf.ia2.transfer.persistence.FileDAO;
Sonia Zorba
committed
import it.inaf.ia2.transfer.persistence.JobDAO;
import it.inaf.ia2.transfer.persistence.LocationDAO;
import it.inaf.ia2.transfer.persistence.model.FileInfo;
import it.inaf.oats.vospace.exception.InternalFaultException;
import it.inaf.oats.vospace.exception.PermissionDeniedException;
import it.inaf.oats.vospace.exception.QuotaExceededException;
import it.inaf.oats.vospace.parent.persistence.LinkedServiceDAO;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
Sonia Zorba
committed
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.security.Principal;
Sonia Zorba
committed
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
Sonia Zorba
committed
import net.ivoa.xml.uws.v1.ExecutionPhase;
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;
Sonia Zorba
committed
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.unit.DataSize;
Sonia Zorba
committed
import org.springframework.web.client.RestTemplate;
@Service
public class ArchiveService {
private static final Logger LOG = LoggerFactory.getLogger(ArchiveService.class);
@Autowired
private FileDAO fileDAO;
Sonia Zorba
committed
@Autowired
private LocationDAO locationDAO;
@Autowired
private LinkedServiceDAO linkedServiceDAO;
Sonia Zorba
committed
@Autowired
private JobDAO jobDAO;
@Autowired
private AuthorizationService authorizationService;
Sonia Zorba
committed
@Autowired
private RestTemplate restTemplate;
@Autowired
private ServletRapClient rapClient;
Sonia Zorba
committed
@Value("${upload_location_id}")
private int uploadLocationId;
// Directory containing temporary files generated by jobs.
Sonia Zorba
committed
@Value("${generated.dir}")
private String generatedDirString;
private File generatedDir;
// Maximum size of the working directory for each registered user
@Value("${generated.dir.max-size}")
private DataSize generatedDirMaxSize;
Sonia Zorba
committed
@PostConstruct
public void init() {
this.generatedDir = new File(generatedDirString);
if (!this.generatedDir.exists()) {
if (!this.generatedDir.mkdirs()) {
throw new IllegalStateException("Unable to create directory " + this.generatedDir.getAbsolutePath());
}
}
}
public <O extends OutputStream, E> void createArchive(ArchiveJob job, HttpServletRequest servletRequest) {
Sonia Zorba
committed
jobDAO.updateJobPhase(ExecutionPhase.EXECUTING, job.getJobId());
LOG.trace("Started archive job " + job.getJobId());
try {
// TODO: check total size limit
Sonia Zorba
committed
File archiveFile = getArchiveFile(job);
String commonParent = getCommonParent(job.getVosPaths());
// support directory used to generate folder inside tar files (path is redefined each time by TarEntry class)
File supportDir = Files.createTempDirectory("dir").toFile();
Sonia Zorba
committed
// it will be initialized only when necessary
Map<Integer, String> portalLocationUrls = null;
try (ArchiveHandler<O, E> handler = getArchiveHandler(archiveFile, job.getType())) {
for (FileInfo fileInfo : fileDAO.getArchiveFileInfos(job.getVosPaths())) {
String relPath = fileInfo.getVirtualPath().substring(commonParent.length());
if (fileInfo.isDirectory()) {
Sonia Zorba
committed
handler.putNextEntry(supportDir, relPath);
// I expect only external links
// local links have been resolved before calling this endpoint
if (fileInfo.isLink()) {
downloadExternalLinkIntoArchive(fileInfo, relPath,
job.getPrincipal(), handler, servletRequest);
continue;
}
Sonia Zorba
committed
if (fileInfo.getLocationId() != null && "portal".equals(fileInfo.getLocationType())) {
Sonia Zorba
committed
// remote file
if (portalLocationUrls == null) {
portalLocationUrls = locationDAO.getPortalLocationUrls();
}
String url = portalLocationUrls.get(fileInfo.getLocationId());
downloadRemoteLocationFileIntoArchive(fileInfo, relPath, job.getPrincipal(), handler, url);
Sonia Zorba
committed
} else {
// local file or virtual directory
writeFileIntoArchive(fileInfo, relPath, job.getPrincipal(), handler);
}
}
} finally {
FileSystemUtils.deleteRecursively(supportDir);
Sonia Zorba
committed
} catch (IOException ex) {
throw new UncheckedIOException(ex);
Sonia Zorba
committed
}
}
private File getArchiveFile(ArchiveJob job) throws IOException {
File parentDir = getArchiveParentDir(job.getPrincipal());
if (!parentDir.exists()) {
if (!parentDir.mkdirs()) {
LOG.error("Unable to create directory " + parentDir.getAbsolutePath());
throw new InternalFaultException("Unable to create temporary directory for job");
Sonia Zorba
committed
}
Sonia Zorba
committed
checkQuotaLimit(parentDir);
Sonia Zorba
committed
File archiveFile = parentDir.toPath().resolve(job.getJobId() + "." + job.getType().getExtension()).toFile();
Sonia Zorba
committed
if (!archiveFile.createNewFile()) {
LOG.error("Unable to create file " + archiveFile.getAbsolutePath());
throw new InternalFaultException("Unable to create archive file");
Sonia Zorba
committed
}
return archiveFile;
}
/**
* If used working space exceeds quota limit throws an
* InsufficientStorageException.
*/
private void checkQuotaLimit(File parentDir) throws IOException {
long usedSpace = Files.walk(parentDir.toPath()).mapToLong(p -> p.toFile().length()).sum();
if (usedSpace > generatedDirMaxSize.toBytes()) {
throw new QuotaExceededException("Archive size limit exceeded.");
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) {
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;
}
private abstract class ArchiveHandler<O extends OutputStream, E> implements AutoCloseable {
Sonia Zorba
committed
private final O os;
private final File parentDir;
ArchiveHandler(O os, File parentDir) {
Sonia Zorba
committed
this.os = os;
this.parentDir = parentDir;
Sonia Zorba
committed
}
public abstract E getEntry(File file, String path);
protected abstract void putNextEntry(E entry) throws IOException;
Sonia Zorba
committed
public final void putNextEntry(File file, String path) throws IOException {
Sonia Zorba
committed
putNextEntry(getEntry(file, path));
checkQuotaLimit(parentDir);
Sonia Zorba
committed
}
public final O getOutputStream() {
return os;
}
Sonia Zorba
committed
@Override
public void close() throws IOException {
Sonia Zorba
committed
os.close();
}
}
private class TarArchiveHandler extends ArchiveHandler<TarOutputStream, TarEntry> {
TarArchiveHandler(File archiveFile) throws IOException {
super(new TarOutputStream(new BufferedOutputStream(new FileOutputStream(archiveFile))), archiveFile.getParentFile());
Sonia Zorba
committed
}
@Override
public TarEntry getEntry(File file, String path) {
return new TarEntry(file, path);
}
@Override
public void putNextEntry(TarEntry entry) throws IOException {
getOutputStream().putNextEntry(entry);
}
}
private class ZipArchiveHandler extends ArchiveHandler<ZipOutputStream, ZipEntry> {
ZipArchiveHandler(File archiveFile) throws IOException {
super(new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(archiveFile))), archiveFile.getParentFile());
Sonia Zorba
committed
}
@Override
public ZipEntry getEntry(File file, String path) {
if (file.isDirectory()) {
// ZipEntry assumes that paths ending with / are folders
path += "/";
Sonia Zorba
committed
return new ZipEntry(path);
}
@Override
public void putNextEntry(ZipEntry entry) throws IOException {
getOutputStream().putNextEntry(entry);
}
}
private ArchiveHandler getArchiveHandler(File archiveFile, ArchiveJob.Type type) throws IOException {
switch (type) {
case TAR:
return new TarArchiveHandler(archiveFile);
case ZIP:
return new ZipArchiveHandler(archiveFile);
default:
throw new UnsupportedOperationException("Type " + type + " not supported yet");
}
}
private <O extends OutputStream, E> void downloadFromUrlIntoArchive(String url, String relPath, TokenPrincipal tokenPrincipal, ArchiveHandler<O, E> handler) {
Sonia Zorba
committed
LOG.trace("Downloading file from " + url);
restTemplate.execute(url, HttpMethod.GET, req -> {
HttpHeaders headers = req.getHeaders();
if (tokenPrincipal.getToken() != null) {
headers.setBearerAuth(tokenPrincipal.getToken());
}
}, res -> {
File tmpFile = Files.createTempFile("download", null).toFile();
try (FileOutputStream os = new FileOutputStream(tmpFile)) {
Sonia Zorba
committed
res.getBody().transferTo(os);
handler.putNextEntry(tmpFile, relPath);
try (FileInputStream is = new FileInputStream(tmpFile)) {
Sonia Zorba
committed
is.transferTo(handler.getOutputStream());
}
} finally {
tmpFile.delete();
}
return null;
}, new Object[]{});
}
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
private <O extends OutputStream, E> void downloadRemoteLocationFileIntoArchive(
FileInfo fileInfo, String relPath, TokenPrincipal tokenPrincipal,
ArchiveHandler<O, E> handler, String baseUrl) {
if (baseUrl == null) {
LOG.error("Location URL not found for location " + fileInfo.getLocationId());
throw new InternalFaultException("Unable to retrieve location of file "
+ fileInfo.getVirtualPath());
}
String url = baseUrl + "/" + fileInfo.getVirtualName();
downloadFromUrlIntoArchive(url, relPath, tokenPrincipal, handler);
}
private <O extends OutputStream, E> void downloadExternalLinkIntoArchive(
FileInfo fileInfo, String relPath, TokenPrincipal tokenPrincipal,
ArchiveHandler<O, E> handler, HttpServletRequest servletRequest) {
String url = fileInfo.getTarget();
if (url == null || url.isBlank()) {
LOG.error("Target URL of link at path: {} is null or blank", fileInfo.getVirtualPath());
throw new InternalFaultException("Target URL of link at path: "
+ fileInfo.getVirtualPath() + " is null or blank");
}
// Append token if url is recognized
if (linkedServiceDAO.isLinkedServiceUrl(url)) {
url += "?token=" + getEndpointToken(tokenPrincipal, url, servletRequest);
}
downloadFromUrlIntoArchive(url, relPath, tokenPrincipal, handler);
}
Sonia Zorba
committed
private <O extends OutputStream, E> void writeFileIntoArchive(FileInfo fileInfo, String relPath, TokenPrincipal tokenPrincipal, ArchiveHandler<O, E> handler) throws IOException {
if (!authorizationService.isDownloadable(fileInfo, tokenPrincipal)) {
throw PermissionDeniedException.forPath(fileInfo.getVirtualPath());
Sonia Zorba
committed
}
Sonia Zorba
committed
LOG.trace("Adding file " + file.getAbsolutePath() + " to tar archive");
try (InputStream is = new FileInputStream(file)) {
Sonia Zorba
committed
handler.putNextEntry(file, relPath);
is.transferTo(handler.getOutputStream());
}
}
private String getEndpointToken(TokenPrincipal tokenPrincipal,
String endpoint, HttpServletRequest servletRequest) {
String token = tokenPrincipal.getToken();
if (token == null) {
throw new PermissionDeniedException("Token is null");
}
TokenExchangeRequest exchangeRequest = new TokenExchangeRequest()
.setSubjectToken(token)
.setResource(endpoint);
// TODO: add audience and scope
return rapClient.exchangeToken(exchangeRequest, servletRequest);
}