mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
ACS-2321 Downloads throws 412 error if content is archived (#822)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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">
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -852,6 +852,12 @@ fileFolderService.checkHidden.enabled=true
|
||||
|
||||
ticket.cleanup.cronExpression=0 0 * * * ?
|
||||
|
||||
#
|
||||
# Maximum number of items to check for archive status when creating downloads node set to -1 for unlimited checks
|
||||
# only used if azure-connector or s3-connector is installed
|
||||
#
|
||||
download.archiveCheckLimit=500
|
||||
|
||||
#
|
||||
# Download Service Cleanup
|
||||
#
|
||||
|
Reference in New Issue
Block a user