From 3aecd0509cff4666aabf3100600ab185eb318381 Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Fri, 12 Nov 2021 14:15:31 +0100 Subject: [PATCH] Added generation of 'Shared files' folder --- .../ia2/vospace/ui/client/VOSpaceClient.java | 33 +++++-- .../ui/controller/NodesController.java | 20 +++- .../ia2/vospace/ui/data/ShareRequest.java | 9 ++ .../ui/service/MainNodesHtmlGenerator.java | 19 +++- .../inaf/ia2/vospace/ui/service/NodeInfo.java | 11 ++- .../ui/service/NodesHtmlGenerator.java | 2 + .../vospace/ui/service/SharingService.java | 92 ++++++++++++++++++- .../vospace/ui/client/VOSpaceClientTest.java | 3 +- .../ui/service/NodesHtmlGeneratorTest.java | 30 ++++++ .../ui/service/SharingServiceTest.java | 70 +++++++++++++- vospace-ui-frontend/src/assets/css/fonts.css | 4 + .../src/components/modal/ShareModal.vue | 22 ++++- 12 files changed, 289 insertions(+), 26 deletions(-) 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 c7c5735..4a33a42 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 abcb6dd..50be555 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 7591f79..a2f20c6 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 4a6ffd2..fe00b1e 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 cb7cc33..b47ef7f 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 8ba5b50..9f3daa4 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 aa6759a..0bfedd6 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 bcf643d..8998550 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 b9d22fa..9738399 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 3f2767d..df918a0 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 e5de317..a03bb16 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 1e4eec5..7fa3f23 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)); } } } -- GitLab