diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java index c7c57354cb9f8f63a0c1b4212ced544692fbe1bc..4a33a42c2a5052b28686c7cc05b4190d08bd057d 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java @@ -12,6 +12,7 @@ import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.exception.BadRequestException; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; +import it.inaf.oats.vospace.datamodel.NodeUtils; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.io.IOException; import java.io.InputStream; @@ -25,6 +26,7 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Scanner; import java.util.concurrent.CompletionException; import java.util.concurrent.ForkJoinPool; @@ -56,9 +58,6 @@ public class VOSpaceClient { @Value("${use-json}") private boolean useJson; - @Value("${vospace-authority}") - private String authority; - private static final ObjectMapper MAPPER = new ObjectMapper(); private final HttpClient httpClient; @@ -84,8 +83,12 @@ public class VOSpaceClient { } public Node getNode(String path) { + return getNode(path, Optional.empty()); + } - HttpRequest request = getRequest("/nodes" + urlEncodePath(path)) + public Node getNode(String path, Optional<String> adminToken) { + + HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken) .header("Accept", useJson ? "application/json" : "text/xml") .build(); @@ -140,10 +143,14 @@ public class VOSpaceClient { } public Node createNode(Node node) { + return createNode(node, Optional.empty()); + } - String path = node.getUri().substring(("vos://" + authority).length()); + public Node createNode(Node node, Optional<String> adminToken) { - HttpRequest request = getRequest("/nodes" + path) + String path = NodeUtils.getVosPath(node); + + HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .PUT(HttpRequest.BodyPublishers.ofString(marshal(node))) @@ -164,10 +171,14 @@ public class VOSpaceClient { } public Node setNode(Node node, boolean recursive) { + return setNode(node, recursive, Optional.empty()); + } + + public Node setNode(Node node, boolean recursive, Optional<String> adminToken) { - String path = node.getUri().substring(("vos://" + authority).length()); + String path = NodeUtils.getVosPath(node); - HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive) + HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive, adminToken) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(node))) @@ -281,8 +292,12 @@ public class VOSpaceClient { } private HttpRequest.Builder getRequest(String path) { + return getRequest(path, Optional.empty()); + } + + private HttpRequest.Builder getRequest(String path, Optional<String> adminToken) { HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(baseUrl + path)); - String token = getToken(); + String token = adminToken.orElseGet(() -> getToken()); if (token != null) { builder.setHeader("Authorization", "Bearer " + token); } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java index abcb6ddbd3a7b1e7d71ee1995392f576f9d90582..50be555ca32b26f7a02967437a662f64cbb4b0cd 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java @@ -17,6 +17,9 @@ import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator; import it.inaf.ia2.vospace.ui.service.MoveOrCopyNodeModalHtmlGenerator; import it.inaf.oats.vospace.datamodel.NodeUtils; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -43,6 +46,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -132,7 +136,9 @@ public class NodesController extends BaseController { for (LinkNode linkNode : linksToLoad) { String prefix = "vos://" + authority; if (linkNode.getTarget().startsWith(prefix)) { - String linkPath = linkNode.getTarget().substring(prefix.length()); + String linkPath = StringUtils.uriDecode(linkNode.getTarget(), StandardCharsets.UTF_8) + .substring(prefix.length()); + nodesCalls.add(CompletableFuture.supplyAsync(() -> client.getNode(linkPath), Runnable::run) .exceptionally(ex -> null)); // null is returned in case of broken link } else { @@ -225,10 +231,10 @@ public class NodesController extends BaseController { transfer.setView(view); if (paths.size() == 1) { - transfer.setTarget("vos://" + authority + paths.get(0)); + transfer.setTarget(getUri(paths.get(0))); } else { String parent = getCommonParent(paths); - transfer.setTarget("vos://" + authority + parent); + transfer.setTarget(getUri(parent)); for (String path : paths) { String childName = path.substring(parent.length() + 1); Param param = new Param(); @@ -246,6 +252,14 @@ public class NodesController extends BaseController { return new Job(client.startTransferJob(transfer), Job.JobType.ARCHIVE); } + private String getUri(String path) { + try { + return new URI("vos", authority, path, null, null).toASCIIString(); + } catch (URISyntaxException ex) { + throw new RuntimeException(ex); + } + } + private String getCommonParent(List<String> vosPaths) { String commonParent = null; for (String vosPath : vosPaths) { diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java index 7591f7998289c7652ce8139f0ca689f92d569b69..a2f20c6e044364b8bd6ce13a69e6b121b4fa79b2 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ShareRequest.java @@ -15,6 +15,7 @@ public class ShareRequest { private List<String> userRead; private List<String> userWrite; private boolean recursive; + private List<String> newPeople; public String getPath() { return path; @@ -63,4 +64,12 @@ public class ShareRequest { public void setRecursive(boolean recursive) { this.recursive = recursive; } + + public List<String> getNewPeople() { + return newPeople; + } + + public void setNewPeople(List<String> newPeople) { + this.newPeople = newPeople; + } } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java index 4a6ffd2c88ab1ad645590c24c7a02db8d9c4c5e4..fe00b1ef56659717247a39a68d0a88141f343925 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java @@ -173,6 +173,10 @@ public class MainNodesHtmlGenerator extends NodesHtmlGenerator { } private boolean isDownloadable(NodeInfo nodeInfo, User user) { + if (nodeInfo.isBrokenLink()) { + return false; + } + if (nodeInfo.isFile()) { if (nodeInfo.isAsyncTrans() || nodeInfo.isBusy()) { return false; @@ -186,11 +190,16 @@ public class MainNodesHtmlGenerator extends NodesHtmlGenerator { return true; } - if (user.getGroups() != null && !user.getGroups().isEmpty() && !nodeInfo.getGroupRead().isEmpty()) { - List<String> groupRead = Arrays.asList(nodeInfo.getGroupRead().split(" ")); - for (String group : groupRead) { - if (user.getGroups().contains(group)) { - return true; + if (user.getGroups() != null) { + if (user.getGroups().contains("VOSpace.ADMIN")) { + return true; + } + if (!user.getGroups().isEmpty() && !nodeInfo.getGroupRead().isEmpty()) { + List<String> groupRead = Arrays.asList(nodeInfo.getGroupRead().split(" ")); + for (String group : groupRead) { + if (user.getGroups().contains(group)) { + return true; + } } } } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java index cb7cc33c04995afc354f5444d8a3288a393eb40f..b47ef7f4afa0408a755a8dd5fd106bc1e6f03950 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodeInfo.java @@ -30,6 +30,7 @@ public class NodeInfo { private final String path; private final String name; private final String size; + private boolean brokenLink; private String type; private final String creator; private final String groupRead; @@ -63,7 +64,11 @@ public class NodeInfo { this.type = linkedNode.getType(); } else if (node instanceof LinkNode) { this.target = ((LinkNode) node).getTarget(); - this.type = "vos:DataNode"; // data link + if (this.target.startsWith("vos://" + authority)) { + this.brokenLink = true; + } else { + this.type = "vos:DataNode"; // external data link + } } } @@ -215,4 +220,8 @@ public class NodeInfo { public boolean isLink() { return target != null; } + + public boolean isBrokenLink() { + return brokenLink; + } } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java index 8ba5b500994b34519b5e4a4b6cdaa95152c89de0..9f3daa44d3cc5e19a64acb51089a4238c52c3c19 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java @@ -74,6 +74,8 @@ public abstract class NodesHtmlGenerator { if (nodeInfo.isLink()) { if (nodeInfo.isFolder()) { icon.addClass("folder-link-icon"); + } else if(nodeInfo.isBrokenLink()) { + icon.addClass("question-icon"); } else { icon.addClass("link-icon"); } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java index aa6759a45ae8ab7305320e32496f8eacd691c2e8..0bfedd6f2d375355e213084d3096761de36e5b8b 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java @@ -16,18 +16,26 @@ import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.ShareRequest; import it.inaf.ia2.vospace.ui.data.SharingInfo; import it.inaf.ia2.vospace.ui.exception.BadRequestException; +import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import it.inaf.oats.vospace.datamodel.NodeProperties; +import it.inaf.oats.vospace.datamodel.NodeUtils; +import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; -import javax.servlet.http.HttpServletRequest; +import net.ivoa.xml.vospace.v2.ContainerNode; +import net.ivoa.xml.vospace.v2.LinkNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Property; import org.slf4j.Logger; @@ -41,9 +49,14 @@ public class SharingService { private static final Logger LOG = LoggerFactory.getLogger(SharingService.class); + private static final String SHARED_FILES_DIR_NAME = "Shared Files"; + @Value("${trusted.eppn.scope}") protected String trustedEppnScope; + @Value("${vospace-authority}") + protected String authority; + private final Set<String> existingGroups = new CopyOnWriteArraySet<>(); private final Set<String> existingPeopleGroups = new CopyOnWriteArraySet<>(); private final Map<String, String> existingUsers = new ConcurrentHashMap<>(); @@ -55,9 +68,6 @@ public class SharingService { private TokenContext tokenContext; private Date lastUpdate; - @Autowired - private HttpServletRequest request; - @Autowired public SharingService(GmsClient gmsClient, ClientCredentialsRapClient gmsRapClient, VOSpaceClient vospaceClient) { this.gmsClient = gmsClient; @@ -100,6 +110,80 @@ public class SharingService { getNodeProperty(node, NodeProperties.GROUP_WRITE_URI).setValue(groupWrite); vospaceClient.setNode(node, shareRequest.isRecursive()); + + if (shareRequest.getNewPeople() != null && !shareRequest.getNewPeople().isEmpty()) { + createSharedLinks(shareRequest); + } + } + + private void createSharedLinks(ShareRequest shareRequest) { + + for (String person : shareRequest.getNewPeople()) { + + loadAccessToken(); + + if (nodeExists("/" + person)) { + + createSharedDir(person); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH.mm.ss"); + String linkNodeName = NodeUtils.getNodeName(shareRequest.getPath()) + " (" + sdf.format(new Date()) + ")"; + + LinkNode sharedLink = new LinkNode(); + sharedLink.setUri("vos://" + authority + urlEncodePath("/" + person + "/" + SHARED_FILES_DIR_NAME + "/" + linkNodeName)); + + try { + URI taregtUri = new URI("vos", authority, shareRequest.getPath(), null, null); + sharedLink.setTarget(taregtUri.toASCIIString()); + } catch (URISyntaxException ex) { + throw new RuntimeException(ex); + } + + addPersonGroups(sharedLink, person); + + vospaceClient.createNode(sharedLink, Optional.of(tokenContext.getAccessToken())); + } + } + } + + private boolean nodeExists(String path) { + try { + vospaceClient.getNode(path, Optional.of(tokenContext.getAccessToken())); + return true; + } catch (VOSpaceStatusException ex) { + if (ex.getHttpStatus() == 404) { + return false; + } else { + throw ex; + } + } + } + + private void createSharedDir(String person) { + ContainerNode sharedFilesDir = new ContainerNode(); + sharedFilesDir.setUri("vos://" + authority + urlEncodePath("/" + person + "/" + SHARED_FILES_DIR_NAME)); + addPersonGroups(sharedFilesDir, person); + + try { + vospaceClient.createNode(sharedFilesDir, Optional.of(tokenContext.getAccessToken())); + } catch (VOSpaceStatusException ex) { + if (ex.getHttpStatus() != 200 && ex.getHttpStatus() != 409) { // created or already existing + throw ex; + } + } + } + + private void addPersonGroups(Node node, String person) { + + Property groupReadProp = new Property(); + groupReadProp.setUri(NodeProperties.GROUP_READ_URI); + groupReadProp.setValue("people." + person.replace(".", "\\.")); + node.getProperties().add(groupReadProp); + + Property groupWriteProp = new Property(); + groupWriteProp.setUri(NodeProperties.GROUP_WRITE_URI); + groupWriteProp.setValue("people." + person.replace(".", "\\.")); + node.getProperties().add(groupWriteProp); } private Property getNodeProperty(Node node, String uri) { diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java index bcf643dcb1ff2b7592f0b22ecdf7425addd87b4d..899855046b9876a64ecd983a0ddabe5cf9b435fb 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java @@ -57,7 +57,6 @@ public class VOSpaceClientTest { try ( MockedStatic<HttpClient> staticMock = Mockito.mockStatic(HttpClient.class)) { staticMock.when(HttpClient::newBuilder).thenReturn(builder); voSpaceClient = new VOSpaceClient("http://localhost/vospace"); - ReflectionTestUtils.setField(voSpaceClient, "authority", "ia2.inaf.it!vospace"); } voSpaceClient.servletRequest = mock(HttpServletRequest.class); @@ -101,7 +100,7 @@ public class VOSpaceClientTest { try { voSpaceClient.createNode(newNode); fail("Exception was expected"); - } catch (IllegalArgumentException ex) { + } catch (RuntimeException ex) { assertTrue(ex.getCause() instanceof URISyntaxException); } } diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java index b9d22fa13cf8ef03a78eaaaa562c643fc15d4b8f..9738399f35e7361a735389e6e98fb9c517f82c88 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java @@ -49,6 +49,11 @@ public class NodesHtmlGeneratorTest { setGroups(link3, "group1", "group2 people.name\\.surname"); parent.getNodes().add(link3); + LinkNode brokenLink = new LinkNode(); + brokenLink.setUri("vos://example.com!vospace/mynode/broken-link"); + brokenLink.setTarget("vos://example.com!vospace/mynode/not-found"); + parent.getNodes().add(brokenLink); + List<Node> linkedNodes = List.of(parent, file1); User user = new User(); @@ -66,6 +71,31 @@ public class NodesHtmlGeneratorTest { assertTrue(html.contains("<span class=\"icon folder-link-icon\"></span> <a href=\"#/nodes/mynode\">link1</a>")); assertTrue(html.contains("<span class=\"icon link-icon\"></span> <a target=\"blank_\" href=\"download/mynode/link2\">link2</a>")); assertTrue(html.contains("<span class=\"icon link-icon\"></span> <a target=\"blank_\" href=\"download/mynode/link3\">link3</a>")); + assertTrue(html.contains("<span class=\"icon question-icon\"></span> broken-link")); + } + + @Test + public void testAdminDownloadable() { + + User user = new User(); + user.setUserId("user_id"); + user.setGroups(Arrays.asList("VOSpace.ADMIN")); + + ContainerNode parent = new ContainerNode(); + parent.setUri("vos://example.com!vospace/mynode"); + setGroups(parent, "group1", "group1"); + + DataNode file1 = new DataNode(); + file1.setUri("vos://example.com!vospace/mynode/file1"); + parent.getNodes().add(file1); + + MainNodesHtmlGenerator generator = new MainNodesHtmlGenerator(parent, user, "example.com!vospace", List.of()); + + String html = generator.generateNodes(); + + System.out.println(html); + + assertTrue(html.contains("<input type=\"checkbox\" data-node=\"/mynode/file1\" class=\"deletable\">")); } private void setGroups(Node node, String groupRead, String groupWrite) { diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java index 3f2767da35d60f3ee95297b559dc2bbcd4b3b903..df918a0e005288fd7f17e3a78adb2c66fda971c9 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java @@ -14,12 +14,16 @@ import it.inaf.ia2.rap.data.RapUser; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.ShareRequest; import it.inaf.ia2.vospace.ui.data.SharingInfo; +import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import it.inaf.oats.vospace.datamodel.NodeProperties; import java.util.ArrayList; import java.util.Arrays; 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.DataNode; +import net.ivoa.xml.vospace.v2.LinkNode; import net.ivoa.xml.vospace.v2.Property; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -33,6 +37,8 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import org.mockito.InjectMocks; import org.mockito.Mock; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -59,8 +65,12 @@ public class SharingServiceTest { @BeforeEach public void setUp() { sharingService.trustedEppnScope = "inaf.it"; + sharingService.authority = "example.com!vospace"; - when(rapClient.getAccessTokenFromClientCredentials()).thenReturn(new AccessTokenResponse()); + AccessTokenResponse tokenResponse = new AccessTokenResponse(); + tokenResponse.setAccessToken("<admin-token>"); + + when(rapClient.getAccessTokenFromClientCredentials()).thenReturn(tokenResponse); when(rapClient.getUsers(anyString(), any())).thenReturn(getRapUsers()); @@ -123,6 +133,64 @@ public class SharingServiceTest { }), anyBoolean()); } + @Test + public void testCreateSharedFolder() { + + DataNode node = new DataNode(); + node.setUri("vos://example.com!vospace/anna.bianchi/mynode"); + + when(vospaceClient.getNode(any())).thenReturn(node); + + ShareRequest shareRequest = new ShareRequest(); + shareRequest.setPath("/anna.bianchi/mynode"); + shareRequest.setUserRead(List.of("mario.rossi", "paolo.gialli", "bianca.verdi")); + shareRequest.setUserWrite(List.of()); + shareRequest.setGroupRead(List.of()); + shareRequest.setGroupWrite(List.of()); + shareRequest.setNewPeople(List.of("mario.rossi", "paolo.gialli", "bianca.verdi")); + + ContainerNode marioHome = new ContainerNode(); + marioHome.setUri("vos://example.com!vospace/mario.rossi"); + ContainerNode biancaHome = new ContainerNode(); + biancaHome.setUri("vos://example.com!vospace/bianca.verdi"); + + // Mario and Bianca have their home, Paolo not + when(vospaceClient.getNode(eq("/mario.rossi"), adminToken())).thenReturn(marioHome); + when(vospaceClient.getNode(eq("/bianca.verdi"), adminToken())).thenReturn(biancaHome); + doThrow(new VOSpaceStatusException("Not found", 404)).when(vospaceClient).getNode(eq("/paolo.gialli"), any()); + + // Mario's Shared Files folder has to be created + ContainerNode marioShared = new ContainerNode(); + marioShared.setUri("vos://example.com!vospace/mario.rossi/Shared%20Files"); + when(vospaceClient.createNode(argThat(n -> n.getUri().equals(marioShared.getUri())), adminToken())).thenReturn(marioShared); + + // Bianca's Shared Files folder already exists + doThrow(new VOSpaceStatusException("Conflict", 409)).when(vospaceClient).createNode( + argThat(n -> n.getUri().equals("vos://example.com!vospace/bianca.verdi/Shared%20Files")), any()); + + doAnswer(inv -> inv.getArgument(0)).when(vospaceClient).createNode(argThat(n -> { + return n instanceof LinkNode + && ((LinkNode) n).getTarget().equals("vos://example.com!vospace/anna.bianchi/mynode"); + }), adminToken()); + + // Share node + sharingService.setNodeGroups(shareRequest); + + // Check Mario's shared link + verify(vospaceClient, times(1)).createNode(argThat(n -> { + return n.getUri().startsWith("vos://example.com!vospace/mario.rossi/Shared%20Files/mynode"); + }), adminToken()); + + // Check Bianca's shared link + verify(vospaceClient, times(1)).createNode(argThat(n -> { + return n.getUri().startsWith("vos://example.com!vospace/bianca.verdi/Shared%20Files/mynode"); + }), adminToken()); + } + + private Optional<String> adminToken() { + return argThat(op -> ((Optional) op).isPresent() && "<admin-token>".equals(((Optional) op).get())); + } + private List<RapUser> getRapUsers() { List<RapUser> users = new ArrayList<>(); diff --git a/vospace-ui-frontend/src/assets/css/fonts.css b/vospace-ui-frontend/src/assets/css/fonts.css index e5de3177f7a8c2e4b32a244cff1d439405101726..a03bb1694c508cdfa971be45cb2cc351c0670e64 100644 --- a/vospace-ui-frontend/src/assets/css/fonts.css +++ b/vospace-ui-frontend/src/assets/css/fonts.css @@ -52,3 +52,7 @@ .link-icon { background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='link45deg' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-link45deg mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M4.715 6.542L3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.001 1.001 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z'%3E%3C/path%3E%3Cpath d='M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 0 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 0 0-4.243-4.243L6.586 4.672z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); } + +.question-icon { + background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='question circle' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-question-circle mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'%3E%3C/path%3E%3Cpath d='M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E"); +} diff --git a/vospace-ui-frontend/src/components/modal/ShareModal.vue b/vospace-ui-frontend/src/components/modal/ShareModal.vue index 1e4eec5976f4eb73b753516a8fc349c5f393373d..7fa3f230039a00653be4caf88528ad09a1aab882 100644 --- a/vospace-ui-frontend/src/components/modal/ShareModal.vue +++ b/vospace-ui-frontend/src/components/modal/ShareModal.vue @@ -39,6 +39,7 @@ export default { userRead: [], groupWrite: [], userWrite: [], + currentPeople: [], people: [], groups: [], recursive: true @@ -48,6 +49,11 @@ export default { onShow() { this.setGroups(this.userRead, this.groupRead, this.node.groupRead); this.setGroups(this.userWrite, this.groupWrite, this.node.groupWrite); + + this.currentPeople.splice(0, this.currentPeople.length); + this.addCurrentPeople(this.currentPeople, this.userRead); + this.addCurrentPeople(this.currentPeople, this.userWrite); + client.getSharingInfo() .then(res => { setArray(this.people, res.people); @@ -65,6 +71,13 @@ export default { } } }, + addCurrentPeople(targetArr, sourceArr) { + for (let person of sourceArr) { + if (!targetArr.includes(person)) { + targetArr.push(person); + } + } + }, share() { client.setNodeGroups({ path: this.$store.state.nodeToShare.path, @@ -72,12 +85,19 @@ export default { groupWrite: this.groupWrite, userRead: this.userRead, userWrite: this.userWrite, - recursive: this.recursive + recursive: this.recursive, + newPeople: this.getNewPeople() }).then(() => { // Reload current path this.$bvModal.hide('share-modal'); this.$store.dispatch('setPath', this.$store.state.path); }); + }, + getNewPeople() { + let currentPeople = []; + this.addCurrentPeople(currentPeople, this.userRead); + this.addCurrentPeople(currentPeople, this.userWrite); + return currentPeople.filter(p => !this.currentPeople.includes(p)); } } }