/* * 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.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.exception.ContainerNotFoundException; import it.inaf.oats.vospace.exception.DuplicateNodeException; import it.inaf.oats.vospace.exception.InternalFaultException; 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; import java.util.List; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; @Service @EnableTransactionManagement public class MoveService { @Autowired private NodeDAO nodeDao; @Value("${vospace-authority}") private String authority; @Autowired private CreateNodeController createNodeController; @Autowired private HttpServletRequest servletRequest; @Transactional(rollbackFor = { Exception.class }) public void processMoveJob(Transfer transfer) { // Get Source Vos Path String sourcePath = transfer.getTarget().substring("vos://".length() + authority.length()); // Get Destination Vos Path (it's in transfer direction) String destinationPath = transfer.getDirection().substring("vos://".length() + authority.length()); // Extract User permissions from servlet request User user = (User) servletRequest.getUserPrincipal(); // Generic common validation for move process job paths this.validatePath(sourcePath); this.validatePath(destinationPath); // Get source node (this locks it with SELECT ... FOR UPDATE) Optional sourceIdOpt = nodeDao.getNodeId(sourcePath); if (sourceIdOpt.isEmpty()) { throw new NodeNotFoundException(sourcePath); } Long sourceId = sourceIdOpt.get(); // Get node branch with root == source. All nodes are locked // with SELECT ... FOR UPDATE List sourceBranchNodeList = nodeDao.listNodesInBranch(sourceId, true); // Check feasibility of move for source branch if (!isWritePermissionsValid(sourceBranchNodeList, user)) { throw new PermissionDeniedException(sourcePath); } if (sourcePath.equals(destinationPath)) { return; } if (!isMoveable(sourceBranchNodeList)) { throw new NodeBusyException(sourcePath); } // Set branch at busy nodeDao.setBranchBusy(sourceId, true); // EDGE CASE: a node with the same destination path is created by another // process in the database between destination check and move. // This applies also to rename. // the move process would overwrite it or worse create two nodes with // different ids and same vos path // possible solution: check for busyness of parent node when creating // a new node? May it work and be compliant? // check if destination node exists before if (this.checkNodeExistence(destinationPath)) { throw new DuplicateNodeException(destinationPath); } // Compare source and destination paths parents and see if it's just a rename if (NodeUtils.getParentPath(sourcePath) .equals(NodeUtils.getParentPath(destinationPath))) { nodeDao.renameNode(sourceId, NodeUtils.getLastPathElement(destinationPath)); } else { Long destParentId; Optional optDest = nodeDao.getNodeId(NodeUtils.getParentPath(destinationPath)); if (optDest.isEmpty()) { // Try to create parent container(s) destParentId = this.createDestination(NodeUtils.getParentPath(destinationPath), user); } else { Node parentNode = nodeDao.getNodeById(optDest.get(), true) .orElseThrow(()-> new NodeNotFoundException(NodeUtils.getParentPath(destinationPath))); this.validateDestinationParentNode(parentNode, user); destParentId = optDest.get(); } this.moveNode(sourceId, destParentId, NodeUtils.getLastPathElement(destinationPath)); } nodeDao.setBranchBusy(sourceId, false); } // All nodes must be writable by the user to have a true private boolean isWritePermissionsValid(List list, User user) { String userName = user.getName(); List userGroups = user.getGroups(); return list.stream().allMatch((n) -> { return NodeUtils.checkIfWritable(n, userName, userGroups); }); } // All nodes must comply to have a true output private boolean isMoveable(List list) { return list.stream().allMatch((n) -> { boolean busy = NodeUtils.getIsBusy(n); boolean sticky = Boolean.valueOf( NodeProperties.getNodePropertyByURI(n, NodeProperties.STICKY_URN)); return (!busy && !sticky); }); } private void moveNode(Long sourceId, Long destParentId, String newNodeName) { nodeDao.moveNodeBranch(sourceId, destParentId); nodeDao.renameNode(sourceId, newNodeName); } private void validatePath(String path) { if (path.equals("/")) { throw new IllegalArgumentException("Cannot move root node or to root node"); } } private boolean checkNodeExistence(String path) { Optional optNodeId = nodeDao.getNodeId(path); return optNodeId.isPresent(); } // Returns node id of created destination private Long createDestination(String path, User user) { List components = NodeUtils.subPathComponents(path); for (int i = 0; i < components.size(); i++) { if (!this.checkNodeExistence(components.get(i))) { ContainerNode node = new ContainerNode(); node.setUri("vos://" + this.authority + components.get(i)); createNodeController.createNode(node, user); } } return nodeDao.getNodeId(path).orElseThrow(()-> new InternalFaultException("Unable to create destination at path: "+path)); } private void validateDestinationParentNode(Node node, User user) { if (!(node instanceof ContainerNode)) { throw new ContainerNotFoundException( NodeUtils.getVosPath(node)); } if (!NodeUtils.checkIfWritable(node, user.getName(), user.getGroups())) { throw new PermissionDeniedException(NodeUtils.getVosPath(node)); } } }