REPO-3718: MNT-19833: Download via v1 REST API loaded in memory (#87)

(cherry picked from commit f3c3b3876f)
Cherry-picked from f3c3b38 master to 6.0.N
This commit is contained in:
Andrei Zapodeanu
2018-10-15 11:40:44 +03:00
committed by Andrei Zapodeanu
parent 0874459dcc
commit 7e5f8fe6fc
6 changed files with 228 additions and 106 deletions

View File

@@ -25,12 +25,13 @@
*/ */
package org.alfresco.repo.web.scripts; package org.alfresco.repo.web.scripts;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.Writer; import java.io.Writer;
import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.error.AlfrescoRuntimeException;
import org.apache.chemistry.opencmis.commons.server.TempStoreOutputStream;
import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.surf.util.StringBuilderWriter; import org.springframework.extensions.surf.util.StringBuilderWriter;
@@ -38,6 +39,7 @@ import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.Runtime; import org.springframework.extensions.webscripts.Runtime;
import org.springframework.extensions.webscripts.WebScriptResponse; import org.springframework.extensions.webscripts.WebScriptResponse;
import org.springframework.extensions.webscripts.WrappingWebScriptResponse; import org.springframework.extensions.webscripts.WrappingWebScriptResponse;
import org.springframework.util.FileCopyUtils;
/** /**
* Transactional Buffered Response * Transactional Buffered Response
@@ -47,9 +49,10 @@ public class BufferedResponse implements WrappingWebScriptResponse
// Logger // Logger
protected static final Log logger = LogFactory.getLog(BufferedResponse.class); protected static final Log logger = LogFactory.getLog(BufferedResponse.class);
private TempStoreOutputStreamFactory streamFactory;
private WebScriptResponse res; private WebScriptResponse res;
private int bufferSize; private int bufferSize;
private ByteArrayOutputStream outputStream = null; private TempStoreOutputStream outputStream = null;
private StringBuilderWriter outputWriter = null; private StringBuilderWriter outputWriter = null;
@@ -59,10 +62,11 @@ public class BufferedResponse implements WrappingWebScriptResponse
* @param res WebScriptResponse * @param res WebScriptResponse
* @param bufferSize int * @param bufferSize int
*/ */
public BufferedResponse(WebScriptResponse res, int bufferSize) public BufferedResponse(WebScriptResponse res, int bufferSize, TempStoreOutputStreamFactory streamFactory)
{ {
this.res = res; this.res = res;
this.bufferSize = bufferSize; this.bufferSize = bufferSize;
this.streamFactory = streamFactory;
} }
/* /*
@@ -129,7 +133,7 @@ public class BufferedResponse implements WrappingWebScriptResponse
{ {
throw new AlfrescoRuntimeException("Already buffering output writer"); throw new AlfrescoRuntimeException("Already buffering output writer");
} }
this.outputStream = new ByteArrayOutputStream(bufferSize); outputStream = streamFactory.newOutputStream();
} }
return outputStream; return outputStream;
} }
@@ -168,7 +172,7 @@ public class BufferedResponse implements WrappingWebScriptResponse
{ {
if (outputStream != null) if (outputStream != null)
{ {
outputStream.reset(); outputStream = null;
} }
else if (outputWriter != null) else if (outputWriter != null)
{ {
@@ -231,7 +235,7 @@ public class BufferedResponse implements WrappingWebScriptResponse
{ {
if (logger.isDebugEnabled() && outputStream != null) if (logger.isDebugEnabled() && outputStream != null)
{ {
logger.debug("Writing Transactional response: size=" + outputStream.size()); logger.debug("Writing Transactional response: size=" + outputStream.getLength());
} }
if (outputWriter != null) if (outputWriter != null)
@@ -242,10 +246,10 @@ public class BufferedResponse implements WrappingWebScriptResponse
else if (outputStream != null) else if (outputStream != null)
{ {
if (logger.isDebugEnabled()) if (logger.isDebugEnabled())
logger.debug("Writing Transactional response: size=" + outputStream.size()); logger.debug("Writing Transactional response: size=" + outputStream.getLength());
outputStream.flush(); outputStream.flush();
outputStream.writeTo(res.getOutputStream()); FileCopyUtils.copy(outputStream.getInputStream(), res.getOutputStream());
} }
} }
catch (IOException e) catch (IOException e)

View File

@@ -480,7 +480,7 @@ public class RepositoryContainer extends AbstractRuntimeContainer
// create buffered request and response that allow transaction retrying // create buffered request and response that allow transaction retrying
bufferedReq = new BufferedRequest(scriptReq, streamFactory); bufferedReq = new BufferedRequest(scriptReq, streamFactory);
bufferedRes = new BufferedResponse(scriptRes, trxParams.getBufferSize()); bufferedRes = new BufferedResponse(scriptRes, trxParams.getBufferSize(), streamFactory);
} }
else else
{ {

View File

@@ -138,7 +138,7 @@ public abstract class ApiWebScript extends AbstractWebScript
protected BufferedResponse getResponse(final WebScriptResponse resp) protected BufferedResponse getResponse(final WebScriptResponse resp)
{ {
// create buffered request and response that allow transaction retrying // create buffered request and response that allow transaction retrying
final BufferedResponse bufferedRes = new BufferedResponse(resp, memoryThreshold); final BufferedResponse bufferedRes = new BufferedResponse(resp, memoryThreshold, streamFactory);
return bufferedRes; return bufferedRes;
} }

View File

@@ -68,6 +68,7 @@ import org.junit.runners.Suite;
org.alfresco.rest.api.tests.TestUserPreferences.class, org.alfresco.rest.api.tests.TestUserPreferences.class,
org.alfresco.rest.api.tests.WherePredicateApiTest.class, org.alfresco.rest.api.tests.WherePredicateApiTest.class,
org.alfresco.rest.api.tests.TestRemovePermissions.class, org.alfresco.rest.api.tests.TestRemovePermissions.class,
org.alfresco.rest.api.tests.BufferedResponseTest.class,
org.alfresco.rest.workflow.api.tests.DeploymentWorkflowApiTest.class, org.alfresco.rest.workflow.api.tests.DeploymentWorkflowApiTest.class,
org.alfresco.rest.workflow.api.tests.ProcessDefinitionWorkflowApiTest.class, org.alfresco.rest.workflow.api.tests.ProcessDefinitionWorkflowApiTest.class,
}) })

View File

@@ -77,7 +77,8 @@ import org.junit.runners.Suite;
TestPublicApi128.class, TestPublicApi128.class,
TestPublicApiCaching.class, TestPublicApiCaching.class,
TestDownloads.class, TestDownloads.class,
AuditAppTest.class AuditAppTest.class,
BufferedResponseTest.class
}) })
public class ApiTest public class ApiTest
{ {

View File

@@ -0,0 +1,116 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2018 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.tests;
import org.alfresco.repo.web.scripts.BufferedResponse;
import org.alfresco.util.TempFileProvider;
import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* Test that BufferedResponse uses a temp file instead of buffering the entire output stream in memory
*
* @author Andrei Zapodeanu
* @author azapodeanu
*/
public class BufferedResponseTest
{
private static final String TEMP_FOLDER_PATH = TempFileProvider.getTempDir().getAbsolutePath();
private static final String TEMP_DIRECTORY_NAME = "testLargeFile";
private static final String LARGE_FILE_NAME = "largeFile.tmp";
private static final String FILE_PREFIX = "opencmis";
private static final Integer LARGE_FILE_SIZE_BYTES = 5 * 1024 * 1024;
private static final Integer MEMORY_THRESHOLD = 4 * 1024 * 1024;
private static final Integer MAX_CONTENT_SIZE = 1024 * 1024 * 1024;
@Before
public void createSourceFile() throws IOException
{
createRandomFileInDirectory(TEMP_FOLDER_PATH, LARGE_FILE_NAME, LARGE_FILE_SIZE_BYTES);
}
@After
public void tearDown() throws Exception
{
File largeFileSource = new File(TEMP_FOLDER_PATH, LARGE_FILE_NAME);
largeFileSource.delete();
}
/**
* Test that the output stream creates a temp file to cache its content when file size was bigger than its memory threshold ( 5 > 4 MB )
* MNT-19833
*/
@Test
public void testOutputStream() throws IOException
{
File bufferTempDirectory = TempFileProvider.getTempDir(TEMP_DIRECTORY_NAME);
TempStoreOutputStreamFactory streamFactory = TempStoreOutputStreamFactory.newInstance(bufferTempDirectory, MEMORY_THRESHOLD, MAX_CONTENT_SIZE,false);
BufferedResponse response = new BufferedResponse(null, 0, streamFactory);
long countBefore = countFilesInDirectoryWithPrefix(bufferTempDirectory, FILE_PREFIX );
copyFileToOutputStream(response);
long countAfter = countFilesInDirectoryWithPrefix(bufferTempDirectory, FILE_PREFIX);
Assert.assertEquals(countBefore + 1, countAfter);
}
private void copyFileToOutputStream(BufferedResponse response) throws IOException
{
File largeFileSource = new File(TEMP_FOLDER_PATH, LARGE_FILE_NAME);
OutputStream testOutputStream = response.getOutputStream();
Files.copy(largeFileSource.toPath(), testOutputStream);
}
private void createRandomFileInDirectory(String path, String fileName, int size) throws IOException
{
String fullPath = new File(path, fileName).getPath();
RandomAccessFile file = new RandomAccessFile(fullPath,"rw");
file.setLength(size);
file.close();
}
private long countFilesInDirectoryWithPrefix(File directory, String filePrefix) throws IOException
{
Stream<File> fileStream = Arrays.stream(directory.listFiles());
return fileStream.filter( f -> f.getName().startsWith(filePrefix)).count();
}
}