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.controller;
import it.inaf.ia2.transfer.persistence.FileDAO;
import it.inaf.ia2.transfer.persistence.ListOfFilesDAO;
Nicola Fulvio Calabria
committed
import it.inaf.ia2.transfer.persistence.JobDAO;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
Nicola Fulvio Calabria
committed
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
Nicola Fulvio Calabria
committed
import javax.xml.bind.DatatypeConverter;
import net.ivoa.xml.uws.v1.ExecutionPhase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
Nicola Fulvio Calabria
committed
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.HttpHeaders;
@RestController
public class PutFileController extends FileController {
private static final Logger LOG = LoggerFactory.getLogger(PutFileController.class);
Nicola Fulvio Calabria
committed
@Autowired
private FileDAO fileDAO;
Nicola Fulvio Calabria
committed
@Autowired
private JobDAO jobDAO;
@Autowired
private ListOfFilesDAO listOfFilesDAO;
@PutMapping("/**")
Nicola Fulvio Calabria
committed
public ResponseEntity<?> putFile(@RequestHeader(value = HttpHeaders.CONTENT_ENCODING, required = false) String contentEncoding,
@RequestParam(value = "file", required = false) MultipartFile file,
@RequestParam(value = "jobid", required = false) String jobId,
HttpServletRequest request) throws IOException, NoSuchAlgorithmException {
Nicola Fulvio Calabria
committed
Nicola Fulvio Calabria
committed
if (jobId == null) {
LOG.debug("putFile called for path {}", path);
} else {
LOG.debug("putFile called for path {} with jobId {}", path, jobId);
if (!jobDAO.isJobExisting(jobId)) {
return new ResponseEntity<>("Job " + jobId + " not found", NOT_FOUND);
Nicola Fulvio Calabria
committed
}
Nicola Fulvio Calabria
committed
}
Optional<FileInfo> optFileInfo = fileDAO.getFileInfo(path);
if (optFileInfo.isPresent()) {
try (InputStream in = file != null ? file.getInputStream() : request.getInputStream()) {
Nicola Fulvio Calabria
committed
FileInfo fileInfo = optFileInfo.get();
if (fileInfo.getAcceptViews() != null && fileInfo.getAcceptViews().contains("urn:list-of-files")) {
storeListOfFiles(fileInfo, in);
} else {
if (file != null) {
fileInfo.setContentType(file.getContentType());
}
Nicola Fulvio Calabria
committed
fileInfo.setContentEncoding(contentEncoding);
Nicola Fulvio Calabria
committed
storeGenericFile(fileInfo, in, jobId);
Nicola Fulvio Calabria
committed
}
}
return ResponseEntity.ok().build();
} else {
return new ResponseEntity<>("File " + path + " not found", NOT_FOUND);
}
}
private void storeListOfFiles(FileInfo fileInfo, InputStream is) throws IOException {
List<String> filePaths = parseListOfFiles(is);
listOfFilesDAO.createList(fileInfo.getVirtualPath(), filePaths);
}
private List<String> parseListOfFiles(InputStream is) throws IOException {
List<String> filePaths = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
String line;
while ((line = br.readLine()) != null && !line.isBlank()) {
filePaths.add(line.trim());
}
}
return filePaths;
}
Nicola Fulvio Calabria
committed
private void storeGenericFile(FileInfo fileInfo, InputStream is, String jobId) throws IOException, NoSuchAlgorithmException {
File file = new File(fileInfo.getOsPath());
/**
* This block must be synchronized, to avoid concurrency issues when
* multiple files are uploaded to a new folder in parallel.
*/
synchronized (this) {
if (!file.getParentFile().exists()) {
if (!file.getParentFile().mkdirs()) {
throw new IllegalStateException("Unable to create parent folder: " + file.getParentFile().getAbsolutePath());
}
String originalFileName = file.getName();
file = getEmptyFile(file, 1);
if (!originalFileName.equals(file.getName())) {
fileDAO.setOsName(fileInfo.getNodeId(), file.getName());
}
try {
fileDAO.setBusy(fileInfo.getNodeId(), true);
Files.copy(is, file.toPath());
if (fileInfo.getContentType() == null) {
fileInfo.setContentType(Files.probeContentType(file.toPath()));
}
Nicola Fulvio Calabria
committed
Long fileSize = Files.size(file.toPath());
String md5Checksum = makeMD5Checksum(file);
fileDAO.updateFileAttributes(fileInfo.getNodeId(),
fileInfo.getContentType(),
fileInfo.getContentEncoding(),
fileSize,
md5Checksum);
Nicola Fulvio Calabria
committed
if (jobId != null) {
jobDAO.updateJobPhase(ExecutionPhase.COMPLETED, jobId);
Nicola Fulvio Calabria
committed
}
} catch (IOException | NoSuchAlgorithmException ex) {
Nicola Fulvio Calabria
committed
jobDAO.updateJobPhase(ExecutionPhase.ERROR, jobId);
Nicola Fulvio Calabria
committed
}
throw ex;
} finally {
fileDAO.setBusy(fileInfo.getNodeId(), false);
}
}
/**
* Handles duplicate file uploads generating a new non existent path. This
* is necessary in some edge cases, like when a file has been renamed in
* VOSpace only but the original file on disk still has the old name or if a
* file has been marked for deletion and a file with the same name is
* uploaded before the cleanup.
*/
private File getEmptyFile(File file, int index) {
if (file.exists()) {
String fileName = file.getName();
String nameWithoutExtension;
String extension = null;
if (fileName.contains(".")) {
nameWithoutExtension = fileName.substring(0, fileName.lastIndexOf("."));
extension = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
} else {
nameWithoutExtension = fileName;
}
Pattern pattern = Pattern.compile("(.*?)-(\\d+)");
Matcher matcher = pattern.matcher(nameWithoutExtension);
if (matcher.matches()) {
nameWithoutExtension = matcher.group(1);
int fileIndex = Integer.parseInt(matcher.group(2));
index = fileIndex + 1;
}
String newName = nameWithoutExtension + "-" + index;
if (extension != null) {
newName += "." + extension;
}
File newFile = file.toPath().getParent().resolve(newName).toFile();
return getEmptyFile(newFile, index + 1);
}
return file;
Nicola Fulvio Calabria
committed
private String makeMD5Checksum(File file) throws NoSuchAlgorithmException, IOException {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(Files.readAllBytes(file.toPath()));
byte[] digest = md.digest();
String checksum = DatatypeConverter.printHexBinary(digest);
return checksum;
}