Skip to content
PutFileController.java 7.39 KiB
Newer Older
Sonia Zorba's avatar
Sonia Zorba committed
/*
 * 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.model.FileInfo;
import it.inaf.ia2.transfer.persistence.FileDAO;
import it.inaf.ia2.transfer.persistence.JobDAO;
import it.inaf.oats.vospace.exception.InvalidArgumentException;
import it.inaf.oats.vospace.exception.NodeNotFoundException;
import it.inaf.oats.vospace.exception.QuotaExceededException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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;
public class PutFileController extends FileController {
    private static final Logger LOG = LoggerFactory.getLogger(PutFileController.class);
    @Autowired
    private FileDAO fileDAO;

    public void 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)) {
                throw new InvalidArgumentException("Job " + jobId + " not found");
        handleFileJob(() -> {
            Optional<FileInfo> optFileInfo = fileDAO.getFileInfo(path);
                FileInfo fileInfo = optFileInfo.get();

                String parentPath = fileInfo.getVirtualPath().substring(0, fileInfo.getVirtualPath().lastIndexOf("/"));
                Long remainingQuota = fileDAO.getRemainingQuota(parentPath);

                // if MultipartFile provides file size it is possible to check
                // quota limit before reading the stream
                if (remainingQuota != null && file != null && file.getSize() > remainingQuota) {
                    throw new QuotaExceededException("Path: " + fileInfo.getVirtualPath());
                }
                
                if (file != null) {
                    fileInfo.setContentType(file.getContentType());
                }
                fileInfo.setContentEncoding(contentEncoding);
                try (InputStream in = file != null ? file.getInputStream() : request.getInputStream()) {
                    storeGenericFile(fileInfo, in, jobId, remainingQuota);
                } catch (IOException | NoSuchAlgorithmException ex) {
                    throw new RuntimeException(ex);
                throw new NodeNotFoundException(path);
    private void storeGenericFile(FileInfo fileInfo, InputStream is, String jobId, Long remainingQuota) 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(), jobId);
            Files.copy(is, file.toPath());

            if (fileInfo.getContentType() == null) {
                fileInfo.setContentType(Files.probeContentType(file.toPath()));
            }


            // Quota limit is checked again to handle cases where MultipartFile is not used
            if (remainingQuota != null && fileSize > remainingQuota) {
                file.delete();
                throw new QuotaExceededException("Path: " + fileInfo.getVirtualPath());
            String md5Checksum = makeMD5Checksum(file);

            fileDAO.updateFileAttributes(fileInfo.getNodeId(),
                    fileInfo.getContentType(),
                    fileInfo.getContentEncoding(),
                    fileSize,
                    md5Checksum);
            fileDAO.setBusy(fileInfo.getNodeId(), null);
        }
    }

    /**
     * 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;
    }