Commit 1caf43e3 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Merge branch 'copyNode'

parents 8154abca 8bd2a20c
Loading
Loading
Loading
Loading
+46 −0
Original line number Original line Diff line number Diff line
/*
 * This file is part of vospace-rest
 * Copyright (C) 2021 Istituto Nazionale di Astrofisica
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package it.inaf.oats.vospace;

import it.inaf.oats.vospace.exception.InternalFaultException;
import it.inaf.oats.vospace.exception.NodeBusyException;
import it.inaf.oats.vospace.exception.PermissionDeniedException;
import it.inaf.oats.vospace.persistence.NodeDAO;
import it.inaf.oats.vospace.persistence.NodeDAO.ShortNodeDescriptor;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

public abstract class AbstractNodeService {
    
    @Autowired
    protected NodeDAO nodeDao;

    @Value("${vospace-authority}")
    protected String authority;

    protected void validatePath(String path) {
        if (path.equals("/")) {
            throw new IllegalArgumentException("Cannot move root node or to root node");
        }
    }

    protected void validateDestinationContainer(ShortNodeDescriptor snd, String destinationVosPath) {
        if (snd.isBusy()) {
            throw new NodeBusyException(destinationVosPath);
        }
        if (snd.isPermissionDenied()) {
            throw new PermissionDeniedException(destinationVosPath);
        }
        if (!snd.isWritable()) {
            throw new InternalFaultException("Destination is not writable: " + destinationVosPath);
        }
        if (!snd.isContainer()) {
            throw new InternalFaultException("Existing destination is not a container: " + destinationVosPath);
        }

    }
}
+117 −0
Original line number Original line Diff line number Diff line
/*
 * This file is part of vospace-rest
 * Copyright (C) 2021 Istituto Nazionale di Astrofisica
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package it.inaf.oats.vospace;

import it.inaf.ia2.aa.data.User;
import it.inaf.oats.vospace.datamodel.NodeUtils;
import it.inaf.oats.vospace.exception.NodeBusyException;
import it.inaf.oats.vospace.exception.NodeNotFoundException;
import it.inaf.oats.vospace.exception.PermissionDeniedException;
import it.inaf.oats.vospace.persistence.NodeDAO.ShortNodeDescriptor;
import java.util.List;
import java.util.Optional;
import net.ivoa.xml.vospace.v2.Transfer;
import org.springframework.dao.CannotSerializeTransactionException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Service
@EnableTransactionManagement
public class CopyService extends AbstractNodeService {


    @Transactional(rollbackFor = {Exception.class}, isolation = Isolation.REPEATABLE_READ)
    public List<String> processCopyNodes(Transfer transfer, String jobId, User user) {

        // Get Source Vos Path
        String sourcePath = URIUtils.returnVosPathFromNodeURI(transfer.getTarget(), authority);

        // Get Destination Vos Path (it's in transfer direction)
        String destinationPath = URIUtils.returnVosPathFromNodeURI(transfer.getDirection(), authority);

        // Destination source to be returned, null if no copy was performed
        String destinationCopyRoot = null;

        this.validatePath(sourcePath);
        this.validatePath(destinationPath);

        if (sourcePath.equals(destinationPath)) {
            throw new IllegalArgumentException("Cannot copy node to itself");
        }

        // Check if destination is subpath of source
        // Linux-like: "cannot copy to a subdirectory of itself" 
        if (destinationPath.startsWith(sourcePath + "/")) {
            throw new IllegalArgumentException("Cannot copy node to a subdirectory of its own path");
        }

        // Check if destination equals parent path of source
        if (NodeUtils.getParentPath(sourcePath).equals(destinationPath)) {
            throw new IllegalArgumentException("Cannot duplicate node at same path without renaming it");
        }

        try {

            // check source branch for read and lock it
            this.checkBranchForReadAndLock(sourcePath, jobId, user);

            // Check destination
            Optional<ShortNodeDescriptor> destShortNodeDescriptor
                    = nodeDao.getShortNodeDescriptor(destinationPath, user.getName(), user.getGroups());

            if (destShortNodeDescriptor.isPresent()) {
                this.validateDestinationContainer(destShortNodeDescriptor.get(), destinationPath);
                destinationCopyRoot = destinationPath + "/" + NodeUtils.getNodeName(sourcePath);

            } else {
                // Check if parent exists
                String destinationParentPath = NodeUtils.getParentPath(destinationPath);
                Optional<ShortNodeDescriptor> destShortNodeDescriptorParent
                        = nodeDao.getShortNodeDescriptor(destinationParentPath, user.getName(), user.getGroups());
                if (destShortNodeDescriptorParent.isPresent()) {
                    this.validateDestinationContainer(destShortNodeDescriptorParent.get(), destinationParentPath);
                    destinationCopyRoot = destinationPath;

                } else {
                    throw new UnsupportedOperationException("Creation of destination upon copy not supported");
                }

            }

            nodeDao.copyBranch(
                    sourcePath,
                    destinationCopyRoot);                        

        } catch (CannotSerializeTransactionException ex) {
            // Concurrent transactions attempted to modify this set of nodes            
            throw new NodeBusyException(sourcePath);
        }
        
        return List.of(sourcePath, destinationCopyRoot);
        
    }

    private void checkBranchForReadAndLock(String sourcePath, String jobId, User user) {

        // Get source node
        Optional<Long> sourceIdOpt = nodeDao.getNodeId(sourcePath);
        long sourceId = sourceIdOpt.orElseThrow(() -> new NodeNotFoundException(sourcePath));

        if (nodeDao.isBranchBusy(sourceId)) {
            throw new NodeBusyException(sourcePath);
        }

        if (!nodeDao.isBranchReadable(sourceId, user.getName(), user.getGroups())) {
            throw new PermissionDeniedException(sourcePath);
        }

        nodeDao.setBranchJobId(sourceId, jobId);

    }

}
+60 −1
Original line number Original line Diff line number Diff line
@@ -87,6 +87,65 @@ public class FileServiceClient {
        }, new Object[]{});
        }, new Object[]{});
    }
    }
    
    
    public void startFileCopyJob(String sourceVosPath, 
            String destiantionVosPath, String jobId, User user) {
        
        CopyRequest copyRequest = new CopyRequest();
        copyRequest.setJobId(jobId);
        copyRequest.setSourceRootVosPath(sourceVosPath);
        copyRequest.setDestinationRootVosPath(destiantionVosPath);
        
        String url = fileServiceUrl + "/copy";
        
        String token = user.getAccessToken();
        restTemplate.execute(url, HttpMethod.POST, req -> {
            HttpHeaders headers = req.getHeaders();
            if (token != null) {
                headers.setBearerAuth(token);
            }

            headers.setContentType(MediaType.APPLICATION_JSON);
            try (OutputStream os = req.getBody()) {
                MAPPER.writeValue(os, copyRequest);
            }
        }, res -> {           
           return null;
        }, new Object[]{});
        
    }

    public static class CopyRequest {

        private String jobId;
        private String sourceRootVosPath;
        private String destinationRootVosPath;

        public String getJobId() {
            return jobId;
        }

        public void setJobId(String jobId) {
            this.jobId = jobId;
        }

        public String getSourceRootVosPath() {
            return sourceRootVosPath;
        }

        public void setSourceRootVosPath(String sourceRootVosPath) {
            this.sourceRootVosPath = sourceRootVosPath;
        }

        public String getDestinationRootVosPath() {
            return destinationRootVosPath;
        }

        public void setDestinationRootVosPath(String destinationRootVosPath) {
            this.destinationRootVosPath = destinationRootVosPath;
        }

    }

    public static class ArchiveRequest {
    public static class ArchiveRequest {


        private String type;
        private String type;
+40 −1
Original line number Original line Diff line number Diff line
@@ -18,6 +18,8 @@ import it.inaf.oats.vospace.exception.InvalidArgumentException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Service;
import it.inaf.oats.vospace.exception.VoSpaceErrorSummarizableException;
import it.inaf.oats.vospace.exception.VoSpaceErrorSummarizableException;
import it.inaf.oats.vospace.persistence.NodeDAO;
import java.util.List;
import java.util.Optional;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Function;
@@ -40,12 +42,21 @@ public class JobService {
    @Autowired
    @Autowired
    private MoveService moveService;
    private MoveService moveService;


    @Autowired
    private CopyService copyService;
    
    @Autowired
    @Autowired
    private AsyncTransferService asyncTransfService;
    private AsyncTransferService asyncTransfService;


    @Autowired
    @Autowired
    private HttpServletRequest servletRequest;
    private HttpServletRequest servletRequest;
    
    
    @Autowired
    private FileServiceClient fileServiceClient;
    
    @Autowired
    private NodeDAO nodeDao;

    public enum JobDirection {
    public enum JobDirection {
        pullToVoSpace,
        pullToVoSpace,
        pullFromVoSpace,
        pullFromVoSpace,
@@ -119,6 +130,9 @@ public class JobService {
                case moveNode:
                case moveNode:
                    handleMoveNode(job, transfer);
                    handleMoveNode(job, transfer);
                    break;
                    break;
                case copyNode:
                    handleCopyNode(job, transfer);
                    break;
                default:
                default:
                    throw new UnsupportedOperationException("Not implemented yet");
                    throw new UnsupportedOperationException("Not implemented yet");
            }
            }
@@ -177,6 +191,31 @@ public class JobService {
        });
        });
    }
    }


    private void handleCopyNode(JobSummary jobSummary, Transfer transfer) {
        // User data must be extracted before starting the new thread
        // to avoid the "No thread-bound request found" exception
        User user = (User) servletRequest.getUserPrincipal();
        CompletableFuture.runAsync(() -> {
            handleJobErrors(jobSummary, job -> {
                
                String jobId = jobSummary.getJobId();
                // Index 0: source 1: destination
                List<String> sourceAndDestination = copyService.processCopyNodes(transfer, jobId, user);
                // Call file service and command copy
                try{                
                fileServiceClient.startFileCopyJob(sourceAndDestination.get(0), sourceAndDestination.get(1), jobId, user);
                } catch (Exception e) {
                    // We decided not to purge metadata in case of failure
                    // just release busy nodes setting job_id = null
                    nodeDao.releaseBusyNodesByJobId(jobId);                    
                    throw e;
                }
                
                return null;
            });
        });
    }

    private void handleJobErrors(JobSummary job, Function<JobSummary, Transfer> jobConsumer) {
    private void handleJobErrors(JobSummary job, Function<JobSummary, Transfer> jobConsumer) {
        Transfer negotiatedTransfer = null;        
        Transfer negotiatedTransfer = null;        
        try {
        try {
+7 −18
Original line number Original line Diff line number Diff line
@@ -11,12 +11,9 @@ import it.inaf.oats.vospace.exception.InternalFaultException;
import it.inaf.oats.vospace.exception.NodeBusyException;
import it.inaf.oats.vospace.exception.NodeBusyException;
import it.inaf.oats.vospace.exception.NodeNotFoundException;
import it.inaf.oats.vospace.exception.NodeNotFoundException;
import it.inaf.oats.vospace.exception.PermissionDeniedException;
import it.inaf.oats.vospace.exception.PermissionDeniedException;
import it.inaf.oats.vospace.persistence.NodeDAO;
import it.inaf.oats.vospace.persistence.NodeDAO.ShortNodeDescriptor;
import it.inaf.oats.vospace.persistence.NodeDAO.ShortNodeDescriptor;
import java.util.Optional;
import java.util.Optional;
import net.ivoa.xml.vospace.v2.Transfer;
import net.ivoa.xml.vospace.v2.Transfer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.CannotSerializeTransactionException;
import org.springframework.dao.CannotSerializeTransactionException;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@@ -25,13 +22,7 @@ import org.springframework.transaction.annotation.Transactional;


@Service
@Service
@EnableTransactionManagement
@EnableTransactionManagement
public class MoveService {
public class MoveService extends AbstractNodeService {

    @Autowired
    private NodeDAO nodeDao;

    @Value("${vospace-authority}")
    private String authority;
    
    
    /**
    /**
     * Perform modeNode operation. User is passed as parameter because this method
     * Perform modeNode operation. User is passed as parameter because this method
@@ -52,7 +43,7 @@ public class MoveService {
        this.validatePath(destinationPath);
        this.validatePath(destinationPath);


        if (sourcePath.equals(destinationPath)) {
        if (sourcePath.equals(destinationPath)) {
            return;
            throw new IllegalArgumentException("Cannot move node to itself");
        }
        }
        
        
        // Check if destination is subpath of source
        // Check if destination is subpath of source
@@ -61,6 +52,11 @@ public class MoveService {
            throw new IllegalArgumentException("Cannot move node to a subdirectory of its own path");
            throw new IllegalArgumentException("Cannot move node to a subdirectory of its own path");
        }
        }


        // Check if destination equals parent path of source
        if(NodeUtils.getParentPath(sourcePath).equals(destinationPath)){
            return;
        }        

        try {
        try {
            // Get source node
            // Get source node
            Optional<Long> sourceIdOpt = nodeDao.getNodeId(sourcePath);
            Optional<Long> sourceIdOpt = nodeDao.getNodeId(sourcePath);
@@ -112,11 +108,4 @@ public class MoveService {
        }
        }
    }  
    }  


    
    private void validatePath(String path) {        
        if (path.equals("/")) {            
            throw new IllegalArgumentException("Cannot move root node or to root node");
        }
    }  

}
}
Loading