diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml index 0bae52055f..0c4cd9b243 100644 --- a/config/alfresco/public-rest-context.xml +++ b/config/alfresco/public-rest-context.xml @@ -509,7 +509,26 @@ + + + + + + + + + org.alfresco.rest.api.Downloads + + + + + + + + + + @@ -771,6 +790,10 @@ + + + + diff --git a/source/java/org/alfresco/rest/api/Downloads.java b/source/java/org/alfresco/rest/api/Downloads.java new file mode 100644 index 0000000000..dea9eb4552 --- /dev/null +++ b/source/java/org/alfresco/rest/api/Downloads.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.Download; + +/** + * downloads API + * + * @author cpopa + * + */ +public interface Downloads +{ + /** + * Creates a download:download node. + * + * @param download + * @return information about the newly created download:download node + */ + Download createDownloadNode(Download download); + + /** + * Get status info about a download node. + * + * @param downloadNodeId + * @return status info about a download:download node + */ + Download getDownloadStatus(String downloadNodeId); + + /** + * Stop the zip creation if still in progress + * @param downloadNodeId + */ + void cancel(String downloadNodeId); +} diff --git a/source/java/org/alfresco/rest/api/downloads/DownloadsEntityResource.java b/source/java/org/alfresco/rest/api/downloads/DownloadsEntityResource.java new file mode 100644 index 0000000000..7cb2616671 --- /dev/null +++ b/source/java/org/alfresco/rest/api/downloads/DownloadsEntityResource.java @@ -0,0 +1,91 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.downloads; + +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.rest.api.Downloads; +import org.alfresco.rest.api.model.Download; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.ParameterCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * + * @author cpopa + * + */ +@EntityResource(name = "downloads", title = "Downloads") +public class DownloadsEntityResource implements EntityResourceAction.Create, EntityResourceAction.ReadById, EntityResourceAction.Delete, InitializingBean +{ + private Downloads downloads; + + public void setDownloads(Downloads downloads) + { + this.downloads = downloads; + } + + @Override + public void afterPropertiesSet() + { + ParameterCheck.mandatory("downloads", this.downloads); + } + + @Override + @WebApiDescription(title = "Create download", description = "Create a download node whose content will be a zip which is being created asynchronously.", successStatus = HttpServletResponse.SC_ACCEPTED) + @WebApiParam(name = "entity", title = "Download request", description = "Download request which contains the node ids for the zip elements.", + kind = ResourceParameter.KIND.HTTP_BODY_OBJECT, allowMultiple = false) + public List create(List entity, Parameters parameters) + { + Download downloadNode = downloads.createDownloadNode(entity.get(0)); + return Collections.singletonList(downloadNode); + } + + @Override + @WebApiDescription(title = "Get download information", description = "Get information about the progress of the zip creation.") + @WebApiParam(name = "nodeId", title = "Download nodeId") + public Download readById(String nodeId, Parameters parameters) throws EntityNotFoundException + { + return downloads.getDownloadStatus(nodeId); + } + + @WebApiDescription(title = "Cancel download", description = "Stop the zip creation if still in progress.", successStatus = HttpServletResponse.SC_ACCEPTED) + @Override + public void delete(String nodeId, Parameters parameters) + { + downloads.cancel(nodeId); + } + +} \ No newline at end of file diff --git a/source/java/org/alfresco/rest/api/downloads/package-info.java b/source/java/org/alfresco/rest/api/downloads/package-info.java new file mode 100644 index 0000000000..7583142401 --- /dev/null +++ b/source/java/org/alfresco/rest/api/downloads/package-info.java @@ -0,0 +1,30 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +@WebApi(name="alfresco", scope=Api.SCOPE.PUBLIC, version=1) +package org.alfresco.rest.api.downloads; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; \ No newline at end of file diff --git a/source/java/org/alfresco/rest/api/impl/DownloadsImpl.java b/source/java/org/alfresco/rest/api/impl/DownloadsImpl.java new file mode 100644 index 0000000000..7dd1a9930d --- /dev/null +++ b/source/java/org/alfresco/rest/api/impl/DownloadsImpl.java @@ -0,0 +1,175 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.impl; + +import java.util.HashSet; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.download.DownloadModel; +import org.alfresco.rest.api.Downloads; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.Download; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.service.cmr.download.DownloadService; +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; + +/** + * + * @author cpopa + * + */ +public class DownloadsImpl implements Downloads +{ + private DownloadService downloadService; + private NodeService nodeService; + private Nodes nodes; + private PermissionService permissionService; + public static final String DEFAULT_ARCHIVE_NAME = "archive.zip"; + public static final String DEFAULT_ARCHIVE_EXTENSION = ".zip"; + + public void setDownloadService(DownloadService downloadService) + { + this.downloadService = downloadService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + @Override + public Download createDownloadNode(Download download) + { + checkEmptyNodeIds(download); + + checkDuplicateNodeId(download); + + NodeRef[] zipContentNodeRefs = validateAndGetNodeRefs(download); + + checkNodeIdsReadPermission(zipContentNodeRefs); + + NodeRef zipNodeRef = downloadService.createDownload(zipContentNodeRefs, true); + + String archiveName = zipContentNodeRefs.length > 1 ? + DEFAULT_ARCHIVE_NAME : + nodeService.getProperty(zipContentNodeRefs[0], ContentModel.PROP_NAME) + DEFAULT_ARCHIVE_EXTENSION; + + nodeService.setProperty(zipNodeRef, ContentModel.PROP_NAME, archiveName); + Download downloadInfo = getStatus(zipNodeRef); + return downloadInfo; + } + + @Override + public Download getDownloadStatus(String downloadNodeId) + { + NodeRef downloadNodeRef = nodes.validateNode(downloadNodeId); + + checkIsDownloadNodeType(downloadNodeRef); + + Download downloadInfo = getStatus(downloadNodeRef); + return downloadInfo; + } + + @Override + public void cancel(String downloadNodeId) + { + NodeRef downloadNodeRef = nodes.validateNode(downloadNodeId); + checkIsDownloadNodeType(downloadNodeRef); + + downloadService.cancelDownload(downloadNodeRef); + } + + protected NodeRef[] validateAndGetNodeRefs(Download download) + { + return download.getNodeIds().stream() + .map(nodeRef -> nodes.validateNode(nodeRef)) + .toArray(NodeRef[]::new); + } + + protected void checkNodeIdsReadPermission(NodeRef[] zipContentNodeRefs) + { + for (NodeRef nodeRef : zipContentNodeRefs) + { + if (permissionService.hasReadPermission(nodeRef).equals(AccessStatus.DENIED)){ + throw new PermissionDeniedException(); + } + } + } + + protected void checkDuplicateNodeId(Download download) + { + if(download.getNodeIds().size() != new HashSet(download.getNodeIds()).size()){ + throw new InvalidArgumentException("Cannot specify the same nodeId twice"); + } + } + + protected void checkEmptyNodeIds(Download download) + { + if (download.getNodeIds().size() == 0) + { + throw new InvalidArgumentException("Cannot create an archive with 0 entries."); + } + } + + protected void checkIsDownloadNodeType(NodeRef downloadNodeRef) + { + QName nodeIdType = this.nodeService.getType(downloadNodeRef); + + if(!nodeIdType.equals(DownloadModel.TYPE_DOWNLOAD)){ + throw new InvalidArgumentException("Please specify the nodeId of a download node."); + } + } + + private Download getStatus(NodeRef downloadNodeRef) + { + DownloadStatus status = downloadService.getDownloadStatus(downloadNodeRef); + Download downloadInfo = new Download(); + downloadInfo.setDownloadId(downloadNodeRef.getId()); + downloadInfo.setDone(status.getDone()); + downloadInfo.setFilesAdded(status.getFilesAdded()); + downloadInfo.setStatus(status.getStatus()); + downloadInfo.setTotalFiles(status.getTotalFiles()); + downloadInfo.setTotal(status.getTotal()); + return downloadInfo; + } + +} diff --git a/source/java/org/alfresco/rest/api/model/Download.java b/source/java/org/alfresco/rest/api/model/Download.java new file mode 100644 index 0000000000..4a00789790 --- /dev/null +++ b/source/java/org/alfresco/rest/api/model/Download.java @@ -0,0 +1,131 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.List; + +import org.alfresco.service.cmr.download.DownloadStatus; + +/** + * Represents a download entity + * + */ +public class Download +{ + private String downloadId; + private List nodeIds; + private DownloadStatus.Status status; + private long done; + private long total; + private long filesAdded; + private long totalFiles; + + public String getDownloadId() + { + return downloadId; + } + + public void setDownloadId(String downloadId) + { + this.downloadId = downloadId; + } + + public List getNodeIds() + { + return nodeIds; + } + + public void setNodeIds(List nodeIds) + { + this.nodeIds = nodeIds; + } + + public DownloadStatus.Status getStatus() + { + return status; + } + + public void setStatus(DownloadStatus.Status status) + { + this.status = status; + } + + public long getDone() + { + return done; + } + + public void setDone(long done) + { + this.done = done; + } + + public long getTotal() + { + return total; + } + + public void setTotal(long total) + { + this.total = total; + } + + public long getFilesAdded() + { + return filesAdded; + } + + public void setFilesAdded(long filesAdded) + { + this.filesAdded = filesAdded; + } + + public long getTotalFiles() + { + return totalFiles; + } + + public void setTotalFiles(long totalFiles) + { + this.totalFiles = totalFiles; + } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(150); + builder.append("Download [downloadId=").append(downloadId) + .append(", nodeIds=").append(nodeIds) + .append(", status=").append(status) + .append(", done=").append(done) + .append(", total=").append(total) + .append(", filesAdded=").append(filesAdded) + .append(", totalFiles=").append(totalFiles) + .append("]"); + return builder.toString(); + } +} diff --git a/source/test-java/org/alfresco/rest/api/tests/ApiTest.java b/source/test-java/org/alfresco/rest/api/tests/ApiTest.java index 1adcc035bc..21d32ee8e0 100644 --- a/source/test-java/org/alfresco/rest/api/tests/ApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/ApiTest.java @@ -74,7 +74,8 @@ import org.junit.runners.Suite; TestSiteMembershipRequests.class, TestFavourites.class, TestPublicApi128.class, - TestPublicApiCaching.class + TestPublicApiCaching.class, + TestDownloads.class }) public class ApiTest { diff --git a/source/test-java/org/alfresco/rest/api/tests/TestDownloads.java b/source/test-java/org/alfresco/rest/api/tests/TestDownloads.java new file mode 100644 index 0000000000..24369d7f1c --- /dev/null +++ b/source/test-java/org/alfresco/rest/api/tests/TestDownloads.java @@ -0,0 +1,520 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.tests; + +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.alfresco.rest.api.impl.DownloadsImpl.DEFAULT_ARCHIVE_EXTENSION; +import static org.alfresco.rest.api.impl.DownloadsImpl.DEFAULT_ARCHIVE_NAME; +import static org.alfresco.rest.api.tests.util.RestApiUtil.toJsonAsStringNonNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; +import org.alfresco.rest.api.model.AssocChild; +import org.alfresco.rest.api.model.Download; +import org.alfresco.rest.api.nodes.NodesEntityResource; +import org.alfresco.rest.api.tests.client.HttpResponse; +import org.alfresco.rest.api.tests.client.PublicApiException; +import org.alfresco.rest.api.tests.client.RequestContext; +import org.alfresco.rest.api.tests.client.data.Document; +import org.alfresco.rest.api.tests.client.data.Folder; +import org.alfresco.rest.api.tests.util.RestApiUtil; +import org.alfresco.rest.framework.core.exceptions.ApiException; +import org.alfresco.service.cmr.download.DownloadStatus; +import org.alfresco.service.cmr.site.SiteVisibility; +import org.json.simple.JSONObject; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * Tests the /downloads API + * + * @author cpopa + * + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class TestDownloads extends AbstractBaseApiTest +{ + public static final String NODES_SECONDARY_CHILDREN = "nodes/%s/secondary-children"; + + public static final String API_DOWNLOADS = "downloads"; + + private static final String DOC4_NAME = "docTest4.txt"; + private static final String SUB_FOLDER1_NAME = "subFolder1"; + private static final String DOC3_NAME = "docTest3.txt"; + private static final String FOLDER1_NAME = "folder1"; + private static final String FOLDER3_NAME = "folder3"; + private static final String ZIPPABLE_DOC1_NAME = "docTest1.txt"; + private static final String DUMMY_CONTENT = "dummy content"; + private org.alfresco.rest.api.Nodes nodesApi; + private String zippableDocId1; + private String zippableDocId2; + private String zippableDocId3_InFolder1; + private String zippableFolderId1; + private String zippableFolderId2_InFolder1; + private String zippableDocId4_InFolder2; + private String zippableFolderId3; + private String zippableDoc_user2; + + @Before + public void setupTest() throws IOException, Exception{ + nodesApi = applicationContext.getBean("Nodes", org.alfresco.rest.api.Nodes.class); + + setRequestContext(user1); + + Document zippableDoc1 = createTextFile(tDocLibNodeId, ZIPPABLE_DOC1_NAME, DUMMY_CONTENT); + zippableDocId1 = zippableDoc1.getId(); + zippableDocId2 = createTextFile(tDocLibNodeId, "docTest2", DUMMY_CONTENT).getId(); + + Folder zippableFolder1 = createFolder(tDocLibNodeId, FOLDER1_NAME); + zippableFolderId1 = zippableFolder1.getId(); + zippableDocId3_InFolder1 = createTextFile(zippableFolderId1, DOC3_NAME, DUMMY_CONTENT).getId(); + + Folder zippableFolder2_InFolder1 = createFolder(zippableFolderId1, SUB_FOLDER1_NAME); + zippableFolderId2_InFolder1 = zippableFolder2_InFolder1.getId(); + + zippableDocId4_InFolder2 = createTextFile(zippableFolderId2_InFolder1, DOC4_NAME, DUMMY_CONTENT).getId(); + + Folder zippableFolder3 = createFolder(tDocLibNodeId, FOLDER3_NAME); + zippableFolderId3 = zippableFolder3.getId(); + + setRequestContext(user2); + String user2Site = createSite ("TestSite B - " + RUNID, SiteVisibility.PRIVATE).getId(); + String user2DocLib = getSiteContainerNodeId(user2Site, "documentLibrary"); + zippableDoc_user2 = createTextFile(user2DocLib, "user2doc", DUMMY_CONTENT).getId(); + + setRequestContext(user1); + AssocChild secChild = new AssocChild(zippableDoc1.getId(), ASSOC_TYPE_CM_CONTAINS); + post(format(NODES_SECONDARY_CHILDREN, zippableFolder3.getId()), toJsonAsStringNonNull(secChild), HttpServletResponse.SC_CREATED); + } + + + /** + * Tests the creation of download nodes. + * + *

POST:

+ * {@literal :/alfresco/api/-default-/private/alfresco/versions/1/downloads} + * + */ + @Test + public void test001CreateDownload() throws Exception + { + //test creating a download with a single file + Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1); + + assertPendingDownloadProps(download); + + assertValidZipNodeid(download); + + assertDoneDownload(download, 1, 13); + + //test creating a multiple file archive + download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableDocId2); + + assertPendingDownloadProps(download); + + assertValidZipNodeid(download); + + assertDoneDownload(download, 2, 26); + + //test creating a zero file archive + createDownload(HttpServletResponse.SC_BAD_REQUEST); + + //test creating an archive with the same file twice + download = createDownload(HttpServletResponse.SC_BAD_REQUEST, zippableDocId1, zippableDocId1); + + //test creating an archive with a folder and a file which is contained in the folder + download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1); + + assertPendingDownloadProps(download); + + assertValidZipNodeid(download); + + assertDoneDownload(download, 3, 39); + + //test creating an archive with a file and a folder containing that file but only as a secondary parent child association + download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableFolderId3); + + assertPendingDownloadProps(download); + + assertValidZipNodeid(download); + + assertDoneDownload(download, 2, 26); + + //test creating an archive with two files, one of which user1 does not have permissions for + download = createDownload(HttpServletResponse.SC_FORBIDDEN, zippableDocId1, zippableDoc_user2); + } + + /** + * Tests retrieving info about a download node + * + *

GET:

+ * {@literal :/alfresco/api/-default-/private/alfresco/versions/1/downloads/} + * + */ + @Test + public void test002GetDownloadInfo() throws Exception + { + Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableFolderId2_InFolder1, zippableDocId4_InFolder2); + + //test retrieving information about an ongoing download + assertInProgressDownload(download, 4, 52); + + //test retrieving information about a finished download + assertDoneDownload(download, 4, 52); + + //test retrieving the status of a cancelled download + download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1); + assertCancelledDownload(download, 3, 39); + } + + /** + * Tests canceling a download. + * + *

DELETE:

+ * {@literal :/alfresco/api/-default-/private/alfresco/versions/1/downloads/} + * + */ + @Test + public void test003CancelDownload() throws Exception + { + //cancel a running download operation + Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableDocId2); + + cancel(download.getDownloadId()); + + assertCancelledDownload(download, 2, 26); + + //cancel a completed download - should have no effect + download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableDocId2); + + assertDoneDownload(download, 2, 26); + + cancel(download.getDownloadId()); + + Thread.sleep(500); + + Download downloadStatus = getDownload(download.getDownloadId()); + + assertTrue("A cancel operation on a DONE download has no effect.", downloadStatus.getStatus().equals(DownloadStatus.Status.DONE)); + + //cancel a node which is not of a download type + cancel(HttpServletResponse.SC_BAD_REQUEST, zippableDocId1); + + //user2 canceling user1 download operation - should not be allowed + download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1); + + publicApiClient.setRequestContext(new RequestContext(networkOne.getId(), user2)); + + cancel(HttpServletResponse.SC_FORBIDDEN, download.getDownloadId()); + } + + /** + * Tests downloading the content of a download node(a zip) using the /nodes API: + * + *

GET:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes//content} + * + */ + @Test + public void test004GetDownloadContent() throws Exception{ + + //test downloading the content of a 1 file zip + Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1); + + assertDoneDownload(download, 1, 13); + + HttpResponse response = downloadContent(download); + + ZipInputStream zipStream = getZipStreamFromResponse(response); + + ZipEntry zipEntry = zipStream.getNextEntry(); + + assertEquals("Zip entry name is not correct", ZIPPABLE_DOC1_NAME, zipEntry.getName()); + + assertTrue("Zip entry size is not correct", zipEntry.getCompressedSize() <= 13); + + assertTrue("No more entries should be in this zip", zipStream.getNextEntry() == null); + zipStream.close(); + + Map responseHeaders = response.getHeaders(); + + assertNotNull(responseHeaders); + + assertEquals(format("attachment; filename=\"%s\"; filename*=UTF-8''%s", ZIPPABLE_DOC1_NAME + DEFAULT_ARCHIVE_EXTENSION, + ZIPPABLE_DOC1_NAME + DEFAULT_ARCHIVE_EXTENSION), + responseHeaders.get("Content-Disposition")); + + //test downloading the content of a multiple file zip + download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableFolderId1, zippableDocId3_InFolder1); + + assertDoneDownload(download, 3, 39); + + response = downloadContent(download); + + zipStream = getZipStreamFromResponse(response); + + assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/", zipStream.getNextEntry().getName()); + assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/" + DOC3_NAME, zipStream.getNextEntry().getName()); + assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/" + SUB_FOLDER1_NAME + "/", zipStream.getNextEntry().getName()); + assertEquals("Zip entry name is not correct", FOLDER1_NAME + "/" + SUB_FOLDER1_NAME + "/" + DOC4_NAME, zipStream.getNextEntry().getName()); + assertEquals("Zip entry name is not correct", DOC3_NAME, zipStream.getNextEntry().getName()); + + assertTrue("No more entries should be in this zip", zipStream.getNextEntry() == null); + zipStream.close(); + + responseHeaders = response.getHeaders(); + + assertNotNull(responseHeaders); + + assertEquals(format("attachment; filename=\"%s\"; filename*=UTF-8''%s", DEFAULT_ARCHIVE_NAME, + DEFAULT_ARCHIVE_NAME), + responseHeaders.get("Content-Disposition")); + + //test download the content of a zip which has a secondary child + download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1, zippableFolderId3); + assertDoneDownload(download, 2, 26); + + response = downloadContent(download); + + zipStream = getZipStreamFromResponse(response); + + assertEquals("Zip entry name is not correct", ZIPPABLE_DOC1_NAME, zipStream.getNextEntry().getName()); + assertEquals("Zip entry name is not correct", FOLDER3_NAME + "/", zipStream.getNextEntry().getName()); + assertEquals("Zip entry name is not correct", FOLDER3_NAME + "/" + ZIPPABLE_DOC1_NAME, zipStream.getNextEntry().getName()); + assertTrue("No more entries should be in this zip", zipStream.getNextEntry() == null); + } + + + protected ZipInputStream getZipStreamFromResponse(HttpResponse response) + { + return new ZipInputStream(new ByteArrayInputStream(response.getResponseAsBytes())); + } + + + protected HttpResponse downloadContent(Download download) throws Exception + { + return getSingle(NodesEntityResource.class, download.getDownloadId() + "/content", null, HttpServletResponse.SC_OK); + } + + /** + * Tests deleting a download node using the /nodes API: + * + *

DELETE:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes/} + * + */ + @Test + public void test005DeleteDownloadNode() throws Exception{ + + //test deleting a download node + Download download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1); + + assertDoneDownload(download, 1, 13); + + deleteNode(download.getDownloadId(), true, HttpServletResponse.SC_NO_CONTENT); + + getDownload(download.getDownloadId(), HttpServletResponse.SC_NOT_FOUND); + + //test user2 deleting a download node created by user1 + download = createDownload(HttpServletResponse.SC_ACCEPTED, zippableDocId1); + + assertDoneDownload(download, 1, 13); + + publicApiClient.setRequestContext(new RequestContext(networkOne.getId(), user2)); + + deleteNode(download.getDownloadId(), true, HttpServletResponse.SC_FORBIDDEN); + + assertDoneDownload(download, 1, 13); + } + + private void assertDoneDownload(Download download, int expectedFilesAdded, int expectedTotal) throws Exception, InterruptedException + { + for(int i = 0; i<=40; i++){ + if (i == 40) + { + fail("Download should be DONE by now."); + } + Download downloadStatus = getDownload(download.getDownloadId()); + if (!downloadStatus.getStatus().equals(DownloadStatus.Status.DONE)){ + Thread.sleep(50); + }else{ + assertTrue("The number of bytes added in the archive does not match the total", downloadStatus.getDone() == downloadStatus.getTotal()); + assertEquals("The number of files added in the archive should be " + expectedFilesAdded, expectedFilesAdded, downloadStatus.getFilesAdded()); + assertEquals("The total number of bytes should be " + expectedTotal, expectedTotal, downloadStatus.getTotal()); + assertEquals("The total number of files of the final archive should be " + expectedFilesAdded, expectedFilesAdded, downloadStatus.getTotalFiles()); + break; + } + } + } + + protected void assertCancelledDownload(Download download, int expectedTotalFiles, int expectedTotal) throws PublicApiException, Exception, InterruptedException + { + cancel(download.getDownloadId()); + for(int i = 0; i<=40; i++){ + if (i == 40) + { + fail("Download should be CANCELLED by now."); + } + Download downloadStatus = getDownload(download.getDownloadId()); + if (!downloadStatus.getStatus().equals(DownloadStatus.Status.CANCELLED)){ + Thread.sleep(50); + }else{ + assertTrue("The total bytes added to the archive by now should be greater than 0", downloadStatus.getDone() > 0 && downloadStatus.getDone() <= downloadStatus.getTotal()); + assertTrue("The download is in progress, there should still be files to be added.", downloadStatus.getFilesAdded() < downloadStatus.getTotalFiles()); + assertEquals("The total number of bytes should be " + expectedTotal, expectedTotal, downloadStatus.getTotal()); + assertEquals("The total number of files to be added to the archive should be " + expectedTotalFiles, expectedTotalFiles, downloadStatus.getTotalFiles()); + break; + } + } + } + + private void assertInProgressDownload(Download download, int expectedTotalFiles, int expectedTotal) throws Exception, InterruptedException + { + for(int i = 0; i<=40; i++){ + if (i == 40) + { + fail("Download creation is taking too long.Download status should be at least IN_PROGRESS by now."); + } + Download downloadStatus = getDownload(download.getDownloadId()); + if (!downloadStatus.getStatus().equals(DownloadStatus.Status.IN_PROGRESS)){ + Thread.sleep(50); + }else{ + //'done' can be equal to the 'total' even though the status is IN_PROGRESS. See ZipDownloadExporter line 239 + assertTrue("The total bytes added to the archive by now should be greater than 0", downloadStatus.getDone() > 0 && downloadStatus.getDone() <= downloadStatus.getTotal()); + assertTrue("The download is in progress, there should still be files to be added.", downloadStatus.getFilesAdded() < downloadStatus.getTotalFiles()); + assertEquals("The total number of bytes should be " + expectedTotal, expectedTotal, downloadStatus.getTotal()); + assertEquals("The total number of files to be added to the archive should be " + expectedTotalFiles, expectedTotalFiles, downloadStatus.getTotalFiles()); + break; + } + } + } + + protected void setRequestContext(String user) + { + setRequestContext(networkOne.getId(), user, null); + } + + private void assertValidZipNodeid(Download download) + { + try{ + TenantUtil.runAsUserTenant(new TenantRunAsWork() + { + + @Override + public Void doWork() throws Exception + { + nodesApi.validateNode(download.getDownloadId()); + return null; + } + }, user1, networkOne.getId()); + + }catch(ApiException ex){ + org.junit.Assert.fail("The download nodeid is not valid." + ex.getMessage()); + } + } + + private void assertPendingDownloadProps(Download download) + { + assertEquals("The download request hasn't been processed yet, the status is not correct", DownloadStatus.Status.PENDING, download.getStatus()); + assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getDone()); + assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getFilesAdded()); + assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getTotal()); + assertEquals("Should be 0, the download req hasn't been processed yet", 0, download.getTotalFiles()); + } + + + @Override + public String getScope() + { + return "public"; + } + + private Download createDownload(int expectedStatus, String ... nodeIds) throws Exception + { + Download downloadRequest = new Download(); + downloadRequest.setNodeIds(Arrays.asList(nodeIds)); + + setRequestContext(user1); + + Download download = create(downloadRequest, expectedStatus); + return download; + } + + public Download create(Download download, int expectedStatus) throws Exception + { + HttpResponse response = post(API_DOWNLOADS, RestApiUtil.toJsonAsStringNonNull(download), expectedStatus); + return getDownloadFromResponse(response); + } + + public Download getDownload(String downloadId, int expectedStatus) throws Exception + { + HttpResponse response = getSingle(API_DOWNLOADS, downloadId, expectedStatus); + + return getDownloadFromResponse(response); + } + + public Download getDownload(String downloadId) throws Exception + { + return getDownload(downloadId, HttpServletResponse.SC_OK); + } + + public void cancel(String downloadId) throws Exception + { + cancel(HttpServletResponse.SC_ACCEPTED, downloadId); + } + + public void cancel(int expectedStatusCode, String downloadId) throws Exception + { + delete(API_DOWNLOADS, downloadId, expectedStatusCode); + } + + protected Download getDownloadFromResponse(HttpResponse response) throws Exception + { + if (asList(SC_ACCEPTED, SC_OK).contains(response.getStatusCode())) + { + return RestApiUtil.parseRestApiEntry((JSONObject) response.getJsonResponse(), Download.class); + } + return null; + } +}