Skip to content
PutFileService.java 7.53 KiB
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.transfer.persistence.FileDAO;
import it.inaf.ia2.transfer.persistence.model.FileInfo;
import it.inaf.oats.vospace.exception.QuotaExceededException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
Nicola Fulvio Calabria's avatar
Nicola Fulvio Calabria committed
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.DatatypeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PutFileService {

    private static final Logger LOG = LoggerFactory.getLogger(PutFileService.class);

    @Autowired
    private FileDAO fileDAO;

    public synchronized void makeFoldersPath(File file) {
        /**
         * This method must be synchronized, to avoid concurrency issues when
         * multiple files are uploaded to a new folder in parallel.
         */
        if (!file.getParentFile().exists()) {
            if (!file.getParentFile().mkdirs()) {
                throw new IllegalStateException("Unable to create parent folder: " + file.getParentFile().getAbsolutePath());
            }
        }
    }

    public void copyLocalFile(FileInfo sourceFileInfo, FileInfo destinationFileInfo, Long remainingQuota) {

        File destinationFile = this.prepareDestination(destinationFileInfo);
        File sourceFile = new File(sourceFileInfo.getOsPath());

        try {
            Files.copy(sourceFile.toPath(), destinationFile.toPath());
            this.finalizeFile(sourceFileInfo, destinationFileInfo, destinationFile, remainingQuota);
Nicola Fulvio Calabria's avatar
Nicola Fulvio Calabria committed
        } catch (IOException e) {
Nicola Fulvio Calabria's avatar
Nicola Fulvio Calabria committed
            throw new UncheckedIOException(e);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    public void storeFileFromInputStream(FileInfo destinationFileInfo,
            InputStream is, Long remainingQuota) {
        this.storeFileFromInputStream(null, destinationFileInfo, is, remainingQuota);
    }

    public void storeFileFromInputStream(FileInfo sourceFileInfo, FileInfo destinationFileInfo,
            InputStream is, Long remainingQuota) {

        File destinationFile = this.prepareDestination(destinationFileInfo);

        try {
            Files.copy(is, destinationFile.toPath());
            this.finalizeFile(sourceFileInfo, destinationFileInfo, destinationFile, remainingQuota);
Nicola Fulvio Calabria's avatar
Nicola Fulvio Calabria committed
        } catch (IOException e) {
            destinationFile.delete();
            throw new UncheckedIOException(e);
        } catch (NoSuchAlgorithmException e) {
            destinationFile.delete();
            throw new RuntimeException(e);
        }

    }

    private File prepareDestination(FileInfo destinationFileInfo) {
        File file = new File(destinationFileInfo.getOsPath());

        makeFoldersPath(file);

        String originalFileName = file.getName();
        file = getEmptyFile(file, 1);
        if (!originalFileName.equals(file.getName())) {
            fileDAO.setOsName(destinationFileInfo.getNodeId(), file.getName());
        }

        return file;
    }

    private Long checkQuota(File destinationFile, Long remainingQuota) throws IOException {
        Long fileSize = Files.size(destinationFile.toPath());

        // Quota limit is checked again to handle cases where MultipartFile is not used
        if (remainingQuota != null && fileSize > remainingQuota) {
            destinationFile.delete();
            throw new QuotaExceededException("Path: " + destinationFile.toPath().toString());
        }

        return fileSize;
    }

    private void finalizeFile(FileInfo sourceFileInfo, FileInfo destinationFileInfo,
            File destinationFile, Long remainingQuota) throws IOException, NoSuchAlgorithmException {

        Long fileSize = this.checkQuota(destinationFile, remainingQuota);

        if (destinationFileInfo.getContentType() == null) {
            destinationFileInfo.setContentType(Files.probeContentType(destinationFile.toPath()));
        }

        String md5Checksum = makeMD5Checksum(destinationFile);

Nicola Fulvio Calabria's avatar
Nicola Fulvio Calabria committed
        // TODO: discuss if mismatches lead to taking actions
        if (sourceFileInfo != null) {
            if (!Objects.equals(sourceFileInfo.getContentLength(), fileSize)) {
                LOG.warn("Destination file {} size mismatch with source", destinationFile.toPath().toString());
            if (sourceFileInfo.getContentType() != null && 
                    !sourceFileInfo.getContentType().equals(destinationFileInfo.getContentType())) {
                LOG.warn("Destination file {} content type mismatch with source {} {}", destinationFile.toPath().toString(),
                        destinationFileInfo.getContentType(), sourceFileInfo.getContentType());
            if (sourceFileInfo.getContentMd5() != null && 
                    !sourceFileInfo.getContentMd5().equals(md5Checksum)) {
                LOG.warn("Destination file {} md5 mismatch with source {} {}", destinationFile.toPath().toString(),
                        destinationFileInfo.getContentMd5(), sourceFileInfo.getContentMd5() );
            }

        }

        fileDAO.updateFileAttributes(destinationFileInfo.getNodeId(),
                destinationFileInfo.getContentType(),
                destinationFileInfo.getContentEncoding(),
                fileSize,
                md5Checksum);
    }

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

}