ACS-2321 Downloads throws 412 error if content is archived (#822)

This commit is contained in:
David Edwards
2021-12-03 18:17:15 +00:00
committed by GitHub
parent 1f18805733
commit c0753e3285
5 changed files with 337 additions and 0 deletions

View File

@@ -25,22 +25,32 @@
*/
package org.alfresco.rest.api.impl;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.ObjectStorageProps;
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.Experimental;
import org.alfresco.service.cmr.download.DownloadService;
import org.alfresco.service.cmr.download.DownloadStatus;
import org.alfresco.service.cmr.module.ModuleService;
import org.alfresco.service.cmr.repository.ArchivedIOException;
import org.alfresco.service.cmr.repository.ContentService;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
@@ -49,23 +59,39 @@ import org.alfresco.service.namespace.QName;
*/
public class DownloadsImpl implements Downloads
{
private static Logger logger = LoggerFactory.getLogger(Downloads.class);
private DownloadService downloadService;
private ModuleService moduleService;
private NodeService nodeService;
private ContentService contentService;
private Nodes nodes;
private PermissionService permissionService;
private int archiveCheckLimit;
public static final String DEFAULT_ARCHIVE_NAME = "archive.zip";
public static final String DEFAULT_ARCHIVE_EXTENSION = ".zip";
public static final String [] CLOUD_CONNECTOR_MODULES = {"org_alfresco_integrations_AzureConnector", "org_alfresco_integrations_S3Connector"};
public void setDownloadService(DownloadService downloadService)
{
this.downloadService = downloadService;
}
public void setModuleService(ModuleService moduleService)
{
this.moduleService = moduleService;
}
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
public void setContentService(ContentService contentService)
{
this.contentService = contentService;
}
public void setNodes(Nodes nodes)
{
this.nodes = nodes;
@@ -76,6 +102,11 @@ public class DownloadsImpl implements Downloads
this.permissionService = permissionService;
}
public void setArchiveCheckLimit(int checkLimit)
{
this.archiveCheckLimit = checkLimit;
}
@Override
public Download createDownloadNode(Download download)
{
@@ -87,6 +118,8 @@ public class DownloadsImpl implements Downloads
checkNodeIdsReadPermission(zipContentNodeRefs);
checkArchiveStatus(zipContentNodeRefs, archiveCheckLimit);
NodeRef zipNodeRef = downloadService.createDownload(zipContentNodeRefs, true);
String archiveName = zipContentNodeRefs.length > 1 ?
@@ -172,4 +205,95 @@ public class DownloadsImpl implements Downloads
return downloadInfo;
}
/**
* Checks the supplied nodes for any content that is archived.
* Any folders will be expanded and their children checked.
* A limit can be applied to prevent large sized requests preventing the asynchronous call to start.
*
* @param nodeRefs
* @param checkLimit The maximum number of nodes to check, set to -1 for no limit
* @see #checkArchiveStatus(NodeRef[], int, Set)
*/
@Experimental
protected void checkArchiveStatus(NodeRef[] nodeRefs, int checkLimit)
{
if (canCheckArchived())
{
checkArchiveStatus(nodeRefs, checkLimit, null);
}
}
/**
* Checks the supplied nodes for any content that is archived.
* Any folders will be expanded and their children checked.
* A limit can be applied to prevent large sized requests preventing the asynchronous call to start.
* The cache is used to prevent duplication of checks, as it is possible to provide a folder and its contents as
* separate nodes in the download request.
*
* @param nodeRefs
* @param checkLimit The maximum number of nodes to check, set to -1 for no limit
* @param cache Tracks nodes that we have already checked, if null an empty cache will be created
*/
@Experimental
private void checkArchiveStatus(NodeRef[] nodeRefs, int checkLimit, Set<NodeRef> cache)
{
// Create the cache for recursive calls.
if (cache == null)
{
cache = new HashSet<NodeRef>();
}
Set<NodeRef> folders = new HashSet<NodeRef>();
for (NodeRef nodeRef : nodeRefs)
{
// We hit the number of nodes we want to check.
if (cache.size() == checkLimit)
{
if (logger.isInfoEnabled())
{
logger.info(
String.format(
"Maximum check of %d reached for archived content. No more checks will be performed and download will still be created.",
checkLimit));
}
return;
}
// Already checked this node, we can skip.
if (cache.contains(nodeRef))
{
continue;
}
QName qName = nodeService.getType(nodeRef);
if (qName.equals(ContentModel.TYPE_FOLDER))
{
// We'll check the child nodes at the end in case there are other nodes in this loop that is archived.
folders.add(nodeRef);
}
else if (qName.equals(ContentModel.TYPE_CONTENT))
{
Map<String, String> props = contentService.getStorageProperties(nodeRef, qName);
if (!props.isEmpty() && Boolean.valueOf(props.get(ObjectStorageProps.X_ALF_ARCHIVED.getValue())))
{
throw new ArchivedIOException("One or more nodes' content is archived and not accessible.");
}
}
cache.add(nodeRef); // No need to check this node again.
}
// We re-run the folder contents at the end in case we hit content that is archived in the first loop and can stop early.
for (NodeRef nodeRef : folders)
{
NodeRef[] childRefs = nodeService.getChildAssocs(nodeRef).stream()
.map(childAssoc -> childAssoc.getChildRef())
.toArray(NodeRef[]::new);
checkArchiveStatus(childRefs, checkLimit, cache); // We'll keep going until we have no more folders in children.
}
}
@Experimental
protected boolean canCheckArchived()
{
return Arrays.stream(CLOUD_CONNECTOR_MODULES).anyMatch(m-> moduleService.getModule(m) != null);
}
}

View File

@@ -565,8 +565,11 @@
<bean id="downloads" class="org.alfresco.rest.api.impl.DownloadsImpl">
<property name="downloadService" ref="DownloadService"/>
<property name="nodeService" ref="NodeService"/>
<property name="contentService" ref="ContentService"/>
<property name="moduleService" ref="ModuleService"/>
<property name="nodes" ref="Nodes" />
<property name="permissionService" ref="permissionService"/>
<property name="archiveCheckLimit" value="${download.archiveCheckLimit}"/>
</bean>
<bean id="actions" class="org.alfresco.rest.api.impl.ActionsImpl">

View File

@@ -76,6 +76,7 @@ import org.junit.runners.Suite;
org.alfresco.repo.web.scripts.site.SurfConfigTest.class,
org.alfresco.repo.web.scripts.node.NodeWebScripTest.class,
org.alfresco.rest.api.impl.CommentsImplUnitTest.class,
org.alfresco.rest.api.impl.DownloadsImplCheckArchiveStatusUnitTest.class,
org.alfresco.rest.api.impl.RestApiDirectUrlConfigUnitTest.class
})
public class AppContext04TestSuite

View File

@@ -0,0 +1,203 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2021 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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api.impl;
import static org.alfresco.model.ContentModel.TYPE_CONTENT;
import static org.alfresco.model.ContentModel.TYPE_FOLDER;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Map;
import org.alfresco.repo.content.ObjectStorageProps;
import org.alfresco.repo.module.ModuleDetailsImpl;
import org.alfresco.rest.api.Nodes;
import org.alfresco.service.cmr.download.DownloadService;
import org.alfresco.service.cmr.module.ModuleDetails;
import org.alfresco.service.cmr.module.ModuleService;
import org.alfresco.service.cmr.repository.ArchivedIOException;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.QName;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class DownloadsImplCheckArchiveStatusUnitTest
{
@InjectMocks
private final DownloadsImpl downloads = new DownloadsImpl();
@Mock
private DownloadService downloadService;
@Mock
private ModuleService moduleService;
@Mock
private NodeService nodeService;
@Mock
private ContentService contentService;
@Mock
private Nodes nodes;
@Mock
private PermissionService permissionService;
private final NodeRef contentNode1 = new NodeRef("://Content:/Node1");
private final NodeRef contentNode2 = new NodeRef("://Content:/Node2");
private final NodeRef contentNode3 = new NodeRef("://Content:/Node3");
private final NodeRef contentNode4 = new NodeRef("://Content:/Node4");
private final NodeRef contentNode5 = new NodeRef("://Content:/Node5");
private final NodeRef contentNode6 = new NodeRef("://Content:/Node6");
private final NodeRef folderParent1 = new NodeRef("://Folder:/Parent1");
private final NodeRef folderParent2 = new NodeRef("://Folder:/Parent2");
private final NodeRef folder1 = new NodeRef("://Folder:/1");
private final NodeRef folder2 = new NodeRef("://Folder:/2");
// folderParent1
private final List<ChildAssociationRef> folderParent1ChildAssocs = List.of(
new ChildAssociationRef(TYPE_FOLDER, folderParent1, TYPE_CONTENT, contentNode3),
new ChildAssociationRef(TYPE_FOLDER, folderParent1, TYPE_CONTENT, contentNode4),
new ChildAssociationRef(TYPE_FOLDER, folderParent1, TYPE_CONTENT, contentNode5),
new ChildAssociationRef(TYPE_FOLDER, folderParent1, TYPE_FOLDER, folder2),
new ChildAssociationRef(TYPE_FOLDER, folderParent1, TYPE_FOLDER, folderParent2)
);
// folderParent2
private final List<ChildAssociationRef> folderParent2ChildAssocs = List.of(
new ChildAssociationRef(TYPE_FOLDER, folderParent2, TYPE_FOLDER, folder1)
);
// folder1
private final List<ChildAssociationRef> folder1ChildAssocs = List.of(
new ChildAssociationRef(TYPE_FOLDER, folder1, TYPE_CONTENT, contentNode1),
new ChildAssociationRef(TYPE_FOLDER, folder1, TYPE_CONTENT, contentNode5),
new ChildAssociationRef(TYPE_FOLDER, folder1, TYPE_CONTENT, contentNode6)
);
// folder2 empty
private final List<ChildAssociationRef> folder2ChildAssocs = List.of();
private final Map<String, String> archivedProps = Map.of(ObjectStorageProps.X_ALF_ARCHIVED.getValue(), "true");
private final Map<String, String> nonArchivedProps = Map.of(ObjectStorageProps.X_ALF_ARCHIVED.getValue(), "false");
private final ModuleDetails mockDetails = new ModuleDetailsImpl("id", null, "title", "description"); // doesn't need to do anything, just exist
private final NodeRef[] nodeRefsToTest = { contentNode1, contentNode2, folderParent1, folder1 };
@Before
public void setup()
{
when(nodeService.getType(any())).thenReturn(TYPE_CONTENT);
when(nodeService.getType(folderParent1)).thenReturn(TYPE_FOLDER);
when(nodeService.getType(folderParent2)).thenReturn(TYPE_FOLDER);
when(nodeService.getType(folder1)).thenReturn(TYPE_FOLDER);
when(nodeService.getType(folder2)).thenReturn(TYPE_FOLDER);
when(nodeService.getChildAssocs(folderParent1)).thenReturn(folderParent1ChildAssocs);
when(nodeService.getChildAssocs(folderParent2)).thenReturn(folderParent2ChildAssocs);
when(nodeService.getChildAssocs(folder1)).thenReturn(folder1ChildAssocs);
when(nodeService.getChildAssocs(folder2)).thenReturn(folder2ChildAssocs);
when(moduleService.getModule("org_alfresco_integrations_S3Connector")).thenReturn(mockDetails);
final NodeRef[] childNodeRefsExpectedFolderParent1 = { contentNode3, contentNode4, contentNode5, folder2, folderParent2 };
final NodeRef[] childNodeRefsExpectedFolderParent2 = { folder1 };
final NodeRef[] childNodeRefsExpectedFolder1 = { contentNode1, contentNode5, contentNode6 };
final NodeRef[] childNodeRefsExpectedFolder2 = {};
assertChildNodeRefMocks(folderParent1, childNodeRefsExpectedFolderParent1);
assertChildNodeRefMocks(folderParent2, childNodeRefsExpectedFolderParent2);
assertChildNodeRefMocks(folder1, childNodeRefsExpectedFolder1);
assertChildNodeRefMocks(folder2, childNodeRefsExpectedFolder2);
}
@Test
public void testAllPass()
{
when(contentService.getStorageProperties(any(NodeRef.class), any(QName.class))).thenReturn(nonArchivedProps);
// No archived nodes, each content node should only be checked once
downloads.checkArchiveStatus(nodeRefsToTest, -1);
verify(contentService, times(1)).getStorageProperties(contentNode1, TYPE_CONTENT);
verify(contentService, times(1)).getStorageProperties(contentNode2, TYPE_CONTENT);
verify(contentService, times(1)).getStorageProperties(contentNode3, TYPE_CONTENT);
verify(contentService, times(1)).getStorageProperties(contentNode4, TYPE_CONTENT);
verify(contentService, times(1)).getStorageProperties(contentNode5, TYPE_CONTENT);
verify(contentService, times(1)).getStorageProperties(contentNode6, TYPE_CONTENT);
verifyNoMoreInteractions(contentService);
}
@Test
public void testFirstItemArchived()
{
when(contentService.getStorageProperties(contentNode1, TYPE_CONTENT)).thenReturn(archivedProps);
assertThrows(ArchivedIOException.class, () -> downloads.checkArchiveStatus(nodeRefsToTest, -1));
verify(contentService, times(1)).getStorageProperties(contentNode1, TYPE_CONTENT);
verifyNoMoreInteractions(contentService);
}
@Test
public void testContentNode3Archived()
{
// fail node3 (within another folder)
when(contentService.getStorageProperties(any(NodeRef.class), any(QName.class))).thenReturn(nonArchivedProps);
when(contentService.getStorageProperties(contentNode3, TYPE_CONTENT)).thenReturn(archivedProps);
assertThrows(ArchivedIOException.class, () -> downloads.checkArchiveStatus(nodeRefsToTest, -1));
verify(contentService, times(1)).getStorageProperties(contentNode1, TYPE_CONTENT);
verify(contentService, times(1)).getStorageProperties(contentNode2, TYPE_CONTENT);
verify(contentService, times(1)).getStorageProperties(contentNode3, TYPE_CONTENT);
verify(contentService, times(1)).getStorageProperties(contentNode5, TYPE_CONTENT);
verify(contentService, times(1)).getStorageProperties(contentNode6, TYPE_CONTENT);
verifyNoMoreInteractions(contentService);
}
private void assertChildNodeRefMocks(final NodeRef nodeRef, final NodeRef[] expecteds)
{
final NodeRef[] actuals = nodeService.getChildAssocs(nodeRef).stream()
.map(childAssoc -> childAssoc.getChildRef())
.toArray(NodeRef[]::new);
assertArrayEquals(expecteds, actuals);
}
}