/* * 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; import it.inaf.ia2.transfer.persistence.JobDAO; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; 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; 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; 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); @Autowired private FileDAO fileDAO; @Autowired private JobDAO jobDAO; @Autowired private ListOfFilesDAO listOfFilesDAO; @PutMapping("/**") 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 { String path = getPath(); 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); } } Optional optFileInfo = fileDAO.getFileInfo(path); if (optFileInfo.isPresent()) { try (InputStream in = file != null ? file.getInputStream() : request.getInputStream()) { 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()); } fileInfo.setContentEncoding(contentEncoding); storeGenericFile(fileInfo, in, jobId); } } return ResponseEntity.ok().build(); } else { return new ResponseEntity<>("File " + path + " not found", NOT_FOUND); } } private void storeListOfFiles(FileInfo fileInfo, InputStream is) throws IOException { List filePaths = parseListOfFiles(is); listOfFilesDAO.createList(fileInfo.getVirtualPath(), filePaths); } private List parseListOfFiles(InputStream is) throws IOException { List 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; } 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())); } Long fileSize = Files.size(file.toPath()); String md5Checksum = makeMD5Checksum(file); fileDAO.updateFileAttributes(fileInfo.getNodeId(), fileInfo.getContentType(), fileInfo.getContentEncoding(), fileSize, md5Checksum); if (jobId != null) { jobDAO.updateJobPhase(ExecutionPhase.COMPLETED, jobId); } } catch (IOException | NoSuchAlgorithmException ex) { if (jobId != null) { jobDAO.updateJobPhase(ExecutionPhase.ERROR, jobId); } 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; } 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; } }