diff --git a/config/alfresco/messages/rest-framework-messages.properties b/config/alfresco/messages/rest-framework-messages.properties index b0de361dd4..024740621f 100644 --- a/config/alfresco/messages/rest-framework-messages.properties +++ b/config/alfresco/messages/rest-framework-messages.properties @@ -11,4 +11,5 @@ framework.exception.PermissionDenied=Permission was denied framework.exception.StaleEntity=Attempt to update a stale entity framework.exception.UnsupportedResourceOperation=The operation is unsupported framework.exception.DeletedResource=In this version of the API resource {0} has been deleted +framework.exception.RequestEntityTooLarge=Request entity too large diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml index 8f53dd0e32..a2d7488a76 100644 --- a/config/alfresco/public-rest-context.xml +++ b/config/alfresco/public-rest-context.xml @@ -137,7 +137,8 @@ - + + diff --git a/source/java/org/alfresco/rest/api/Nodes.java b/source/java/org/alfresco/rest/api/Nodes.java index 3627e0f3ff..7a25104c3b 100644 --- a/source/java/org/alfresco/rest/api/Nodes.java +++ b/source/java/org/alfresco/rest/api/Nodes.java @@ -38,6 +38,7 @@ import org.alfresco.rest.framework.resource.parameters.Parameters; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.namespace.QName; +import org.springframework.extensions.webscripts.servlet.FormData; /** * @author steveglover @@ -45,11 +46,11 @@ import org.alfresco.service.namespace.QName; */ public interface Nodes { - NodeRef validateNode(StoreRef storeRef, String nodeId); - NodeRef validateNode(String nodeId); - NodeRef validateNode(NodeRef nodeRef); - boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes); - + NodeRef validateNode(StoreRef storeRef, String nodeId); + NodeRef validateNode(String nodeId); + NodeRef validateNode(NodeRef nodeRef); + boolean nodeMatches(NodeRef nodeRef, Set expectedTypes, Set excludedTypes); + /** * Get the node representation for the given node. * @param nodeId String @@ -122,4 +123,14 @@ public interface Nodes // TODO update REST fwk - to optionally support return of json void updateContent(String fileNodeId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters); + + /** + * Uploads file content and meta-data into the repository. + * + * @param parentFolderNodeId String id of parent folder node or well-known alias, eg. "-root-" or "-my-" + * @param formData the {@link FormData} + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return {@code Node} if successful + */ + Node upload(String parentFolderNodeId, FormData formData, Parameters parameters); } diff --git a/source/java/org/alfresco/rest/api/impl/NodesImpl.java b/source/java/org/alfresco/rest/api/impl/NodesImpl.java index e484533b79..9b92d51d3e 100644 --- a/source/java/org/alfresco/rest/api/impl/NodesImpl.java +++ b/source/java/org/alfresco/rest/api/impl/NodesImpl.java @@ -28,8 +28,10 @@ package org.alfresco.rest.api.impl; import org.alfresco.model.ContentModel; import org.alfresco.query.PagingRequest; import org.alfresco.query.PagingResults; +import org.alfresco.repo.content.ContentLimitViolationException; import org.alfresco.repo.model.Repository; import org.alfresco.repo.node.getchildren.GetChildrenCannedQuery; +import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.rest.antlr.WhereClauseParser; import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.model.Document; @@ -38,8 +40,12 @@ import org.alfresco.rest.api.model.Node; import org.alfresco.rest.api.model.PathInfo; import org.alfresco.rest.api.model.PathInfo.ElementInfo; import org.alfresco.rest.api.model.UserInfo; +import org.alfresco.rest.framework.core.exceptions.ApiException; +import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.core.exceptions.RequestEntityTooLargeException; import org.alfresco.rest.framework.resource.content.BasicContentInfo; import org.alfresco.rest.framework.resource.content.BinaryResource; import org.alfresco.rest.framework.resource.content.NodeBinaryResource; @@ -51,14 +57,19 @@ import org.alfresco.rest.framework.resource.parameters.where.Query; import org.alfresco.rest.framework.resource.parameters.where.QueryHelper; import org.alfresco.rest.workflow.api.impl.MapBasedQueryWalker; import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionDefinition; +import org.alfresco.service.cmr.action.ActionService; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.Path; @@ -66,13 +77,23 @@ import org.alfresco.service.cmr.repository.Path.Element; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.usage.ContentQuotaException; +import org.alfresco.service.cmr.version.VersionService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.Pair; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.Content; +import org.springframework.extensions.webscripts.servlet.FormData; +import java.io.BufferedInputStream; +import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.math.BigInteger; +import java.nio.charset.Charset; import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; @@ -95,6 +116,8 @@ import java.util.Set; */ public class NodesImpl implements Nodes { + private static final Log logger = LogFactory.getLog(NodesImpl.class); + private static enum Type { // Note: ordered @@ -114,6 +137,10 @@ public class NodesImpl implements Nodes private FileFolderService fileFolderService; private NamespaceService namespaceService; private PermissionService permissionService; + private MimetypeService mimetypeService; + private ContentService contentService; + private ActionService actionService; + private VersionService versionService; private Repository repositoryHelper; private ServiceRegistry sr; private Set defaultIgnoreTypes; @@ -121,6 +148,16 @@ public class NodesImpl implements Nodes public void init() { + this.namespaceService = sr.getNamespaceService(); + this.fileFolderService = sr.getFileFolderService(); + this.nodeService = sr.getNodeService(); + this.permissionService = sr.getPermissionService(); + this.dictionaryService = sr.getDictionaryService(); + this.mimetypeService = sr.getMimetypeService(); + this.contentService = sr.getContentService(); + this.actionService = sr.getActionService(); + this.versionService = sr.getVersionService(); + if (defaultIgnoreTypes != null) { ignoreTypeQNames = new HashSet<>(defaultIgnoreTypes.size()); @@ -133,12 +170,6 @@ public class NodesImpl implements Nodes public void setServiceRegistry(ServiceRegistry sr) { this.sr = sr; - - this.namespaceService = sr.getNamespaceService(); - this.fileFolderService = sr.getFileFolderService(); - this.nodeService = sr.getNodeService(); - this.permissionService = sr.getPermissionService(); - this.dictionaryService = sr.getDictionaryService(); } public void setRepositoryHelper(Repository repositoryHelper) @@ -792,15 +823,15 @@ public class NodesImpl implements Nodes } // TODO should we able to specify content properties (eg. mimeType ... or use extension for now, or encoding) - public Node createNode(String parentFolderNodeId, Node nodeInfo, Parameters parameters) - { + public Node createNode(String parentFolderNodeId, Node nodeInfo, Parameters parameters) + { // check that requested parent node exists and it's type is a (sub-)type of folder final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null); - if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null)) - { - throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef); - } + if (! nodeMatches(parentNodeRef, Collections.singleton(ContentModel.TYPE_FOLDER), null)) + { + throw new InvalidArgumentException("NodeId of folder is expected: "+parentNodeRef); + } String nodeName = nodeInfo.getName(); if ((nodeName == null) || nodeName.isEmpty()) @@ -849,7 +880,7 @@ public class NodesImpl implements Nodes } return getFolderOrDocument(nodeRef.getId(), parameters); - } + } public Node updateNode(String nodeId, Node nodeInfo, Parameters parameters) { @@ -933,6 +964,259 @@ public class NodesImpl implements Nodes return; } + @Override + public Node upload(String parentFolderNodeId, FormData formData, Parameters parameters) + { + if (formData == null || !formData.getIsMultiPart()) + { + throw new InvalidArgumentException("The request content-type is not multipart"); + } + + final NodeRef parentNodeRef = validateOrLookupNode(parentFolderNodeId, null); + if (Type.DOCUMENT == getType(parentNodeRef)) + { + throw new InvalidArgumentException(parentFolderNodeId + " is not a folder."); + } + + String fileName = null; + Content content = null; + boolean overwrite = false; // If a fileName clashes for a versionable file + + for (FormData.FormField field : formData.getFields()) + { + switch (field.getName().toLowerCase()) + { + case "filename": + fileName = getStringOrNull(field.getValue()); + break; + + case "filedata": + if (field.getIsFile()) + { + fileName = fileName != null ? fileName : field.getFilename(); + content = field.getContent(); + } + break; + + case "overwrite": + overwrite = Boolean.valueOf(field.getValue()); + break; + } + } + + try + { + // MNT-7213 When alf_data runs out of disk space, Share uploads + // result in a success message, but the files do not appear. + if (formData.getFields().length == 0) + { + throw new ConstraintViolatedException(" No disk space available"); + } + + // Ensure mandatory file attributes have been located. Need either + // destination, or site + container or updateNodeRef + if ((fileName == null || content == null)) + { + throw new InvalidArgumentException("Required parameters are missing"); + } + /* + * Existing file handling + */ + NodeRef existingFile = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, fileName); + if (existingFile != null) + { + // File already exists, decide what to do + if (overwrite && nodeService.hasAspect(existingFile, ContentModel.ASPECT_VERSIONABLE)) + { + // Upload component was configured to overwrite files if name clashes + write(existingFile, content, fileName, false, true); + + // Extract the metadata (The overwrite policy controls + // which if any parts of the document's properties are updated from this) + extractMetadata(existingFile); + + // Do not clean formData temp files to + // allow for retries. Temp files will be deleted later + // when GC call DiskFileItem#finalize() method or by temp file cleaner. + return createUploadResponse(parentNodeRef, existingFile); + } + else + { + throw new ConstraintViolatedException(fileName + " already exists."); + } + } + + // Create a new file. + return createNewFile(parentNodeRef, fileName, content); + + // Do not clean formData temp files to allow for retries. + // Temp files will be deleted later when GC call DiskFileItem#finalize() method or by temp file cleaner. + } + catch (ApiException apiEx) + { + // As this is an public API fwk exception, there is no need to convert it, so just throw it. + throw apiEx; + } + catch (AccessDeniedException ade) + { + throw new PermissionDeniedException(); + } + catch (ContentQuotaException cqe) + { + throw new RequestEntityTooLargeException(); + } + catch (ContentLimitViolationException clv) + { + throw new ConstraintViolatedException(); + } + catch (Exception ex) + { + /* + * NOTE: Do not clean formData temp files to allow for retries. It's + * possible for a temp file to remain if max retry attempts are + * made, but this is rare, so leave to usual temp file cleanup. + */ + + throw new ApiException("Unexpected error occurred during upload of new content.", ex); + } + } + + /** + * Helper to create a new node and writes its content to the repository. + */ + private Node createNewFile(NodeRef parentNodeRef, String fileName, Content content) + { + FileInfo fileInfo = fileFolderService.create(parentNodeRef, fileName, ContentModel.TYPE_CONTENT); + NodeRef newFile = fileInfo.getNodeRef(); + + // Write content + write(newFile, content, fileName, false, true); + + // Ensure the file is versionable (autoVersion = true, autoVersionProps = false) + ensureVersioningEnabled(newFile, true, false); + + // Extract the metadata + extractMetadata(newFile); + + // Create the response + return createUploadResponse(parentNodeRef, newFile); + } + + private Node createUploadResponse(NodeRef parentNodeRef, NodeRef newFileNodeRef) + { + return getFolderOrDocument(newFileNodeRef, parentNodeRef, ContentModel.TYPE_CONTENT, Collections.emptyList(), true, null); + } + + private String getStringOrNull(String value) + { + if (StringUtils.isNotEmpty(value)) + { + return value.equalsIgnoreCase("null") ? null : value; + } + return null; + } + + /** + * Writes the content to the repository. + * + * @param nodeRef the reference to the node having a content property + * @param content the content + * @param fileName the uploaded file name + * @param applyMimeType If true, apply the mimeType from the Content object, + * else leave the original mimeType + * @param guessEncoding If true, guess the encoding from the underlying + * input stream, else use encoding set in the Content object as supplied + */ + protected void write(NodeRef nodeRef, Content content, String fileName, boolean applyMimeType, boolean guessEncoding) + { + ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); + InputStream is; + String mimeType = content.getMimetype(); + if (!applyMimeType) + { + ContentData existingContentData = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT); + if (existingContentData != null) + { + mimeType = existingContentData.getMimetype(); + } + else + { + mimeType = mimetypeService.guessMimetype(fileName); + } + } + if (guessEncoding) + { + is = new BufferedInputStream(content.getInputStream()); + is.mark(1024); + + writer.setEncoding(guessEncoding(is, mimeType)); + try + { + is.reset(); + } + catch (IOException e) + { + logger.error("Failed to reset input stream", e); + } + } + else + { + writer.setEncoding(content.getEncoding()); + is = content.getInputStream(); + } + writer.setMimetype(mimeType); + writer.putContent(is); + } + + /** + * Ensures the given node has the {@code cm:versionable} aspect applied to it, and + * that it has the initial version in the version store. + * + * @param nodeRef the reference to the node to be checked + * @param autoVersion If the {@code cm:versionable} aspect is applied, should auto + * versioning be requested? + * @param autoVersionProps If the {@code cm:versionable} aspect is applied, should + * auto versioning of properties be requested? + */ + protected void ensureVersioningEnabled(NodeRef nodeRef, boolean autoVersion, boolean autoVersionProps) + { + Map props = new HashMap<>(2); + props.put(ContentModel.PROP_AUTO_VERSION, autoVersion); + props.put(ContentModel.PROP_AUTO_VERSION_PROPS, autoVersionProps); + + versionService.ensureVersioningEnabled(nodeRef, props); + } + + /** + * Guesses the character encoding of the given inputStream. + */ + protected String guessEncoding(InputStream in, String mimeType) + { + String encoding = "UTF-8"; + + if (in != null) + { + Charset charset = mimetypeService.getContentCharsetFinder().getCharset(in, mimeType); + encoding = charset.name(); + } + + return encoding; + } + + /** + * Extracts the given node metadata asynchronously. + */ + private void extractMetadata(NodeRef nodeRef) + { + final String actionName = "extract-metadata"; + ActionDefinition actionDef = actionService.getActionDefinition(actionName); + if (actionDef != null) + { + Action action = actionService.createAction(actionName); + actionService.executeAction(action, nodeRef); + } + } + /** * Helper to create a QName from either a fully qualified or short-name QName string * diff --git a/source/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java b/source/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java index 723f7104e2..07e2a9834d 100644 --- a/source/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java +++ b/source/java/org/alfresco/rest/api/nodes/NodeChildrenRelation.java @@ -19,15 +19,17 @@ package org.alfresco.rest.api.nodes; import org.alfresco.rest.api.Nodes; -import org.alfresco.rest.api.model.Folder; import org.alfresco.rest.api.model.Node; import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.MultiPartRelationshipResourceAction; import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Parameters; import org.alfresco.util.ParameterCheck; import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.webscripts.servlet.FormData; import java.util.ArrayList; import java.util.List; @@ -36,18 +38,20 @@ import java.util.List; * TODO ... work-in-progress * * @author janv + * @author Jamal Kaabi-Mofrad */ @RelationshipResource(name = "children", entityResource = NodesEntityResource.class, title = "Folder children") -public class NodeChildrenRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.Create, InitializingBean +public class NodeChildrenRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.Create, + MultiPartRelationshipResourceAction.Create, InitializingBean { - private Nodes nodes; + private Nodes nodes; public void setNodes(Nodes nodes) { this.nodes = nodes; } - @Override + @Override public void afterPropertiesSet() { ParameterCheck.mandatory("nodes", this.nodes); @@ -104,4 +108,13 @@ public class NodeChildrenRelation implements RelationshipResourceAction.Read. + */ + +package org.alfresco.rest.framework.core.exceptions; + +/** + * @author Jamal Kaabi-Mofrad + */ +public class RequestEntityTooLargeException extends ApiException +{ + private static final long serialVersionUID = 3196212354672333823L; + + public static String DEFAULT_MESSAGE_ID = "framework.exception.RequestEntityTooLarge"; + + public RequestEntityTooLargeException() + { + super(DEFAULT_MESSAGE_ID); + } + + public RequestEntityTooLargeException(String msgId) + { + super(msgId); + } +} diff --git a/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java b/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java index 6d16885058..1c1efd7249 100644 --- a/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/NodeApiTest.java @@ -19,18 +19,22 @@ package org.alfresco.rest.api.tests; +import static org.alfresco.rest.api.tests.util.RestApiUtil.parsePaging; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import org.alfresco.model.ContentModel; import org.alfresco.model.ForumModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.model.Repository; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.rest.api.model.ContentInfo; import org.alfresco.rest.api.model.Document; +import org.alfresco.rest.api.model.Folder; import org.alfresco.rest.api.model.Node; import org.alfresco.rest.api.model.PathInfo; import org.alfresco.rest.api.model.PathInfo.ElementInfo; @@ -40,10 +44,14 @@ import org.alfresco.rest.api.tests.RepoService.TestNetwork; import org.alfresco.rest.api.tests.RepoService.TestPerson; import org.alfresco.rest.api.tests.RepoService.TestSite; import org.alfresco.rest.api.tests.client.HttpResponse; +import org.alfresco.rest.api.tests.client.PublicApiClient; import org.alfresco.rest.api.tests.client.PublicApiClient.ExpectedPaging; import org.alfresco.rest.api.tests.client.PublicApiClient.Paging; import org.alfresco.rest.api.tests.client.data.SiteRole; import org.alfresco.rest.api.tests.util.JacksonUtil; +import org.alfresco.rest.api.tests.util.MultiPartBuilder; +import org.alfresco.rest.api.tests.util.MultiPartBuilder.FileData; +import org.alfresco.rest.api.tests.util.MultiPartBuilder.MultiPartRequest; import org.alfresco.rest.api.tests.util.RestApiUtil; import org.alfresco.rest.framework.jacksonextensions.JacksonHelper; import org.alfresco.service.cmr.repository.NodeRef; @@ -54,8 +62,12 @@ import org.alfresco.service.cmr.site.SiteVisibility; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.springframework.util.ResourceUtils; +import java.io.File; +import java.io.FileNotFoundException; import java.io.Serializable; +import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -67,14 +79,16 @@ import java.util.Set; /** * API tests for: *
    - *
  • {@literal host:port/alfresco/api/{networkId}/public/alfresco/versions/1/nodes/{nodeId}}
  • - *
  • {@literal host:port/alfresco/api/{networkId}/public/alfresco/versions/1/nodes/{nodeId}/children}
  • + *
  • {@literal :/alfresco/api//public/alfresco/versions/1/nodes/}
  • + *
  • {@literal :/alfresco/api//public/alfresco/versions/1/nodes//children}
  • *
* * @author Jamal Kaabi-Mofrad */ public class NodeApiTest extends AbstractBaseApiTest { + private static final String RESOURCE_PREFIX = "publicapi/upload/"; + /** * User one from network one */ @@ -145,6 +159,11 @@ public class NodeApiTest extends AbstractBaseApiTest AuthenticationUtil.clearCurrentSecurityContext(); } + /** + * Tests get document library children. + *

GET:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes//children} + */ @Test public void testListDocLibChildren() throws Exception { @@ -246,6 +265,11 @@ public class NodeApiTest extends AbstractBaseApiTest getAll(getChildrenUrl(docLibNodeRef), userTwoN1.getId(), paging, 403); } + /** + * Tests get user's home children. + *

GET:

+ * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes//children} + */ @Test public void testListMyFilesChildren() throws Exception { @@ -317,6 +341,11 @@ public class NodeApiTest extends AbstractBaseApiTest assertEquals("doclib:1444660852296", ((List) entry.getValue()).get(0)); } + /** + * Tests get node with path information. + *

GET:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes/?select=path} + */ @Test public void testGetPathElements_DocLib() throws Exception { @@ -385,6 +414,11 @@ public class NodeApiTest extends AbstractBaseApiTest assertEquals(folderC, pathElements.get(0).getName()); } + /** + * Tests get node with path information. + *

GET:

+ * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes/?select=path} + */ @Test public void testGetPathElements_MyFiles() throws Exception { @@ -434,6 +468,14 @@ public class NodeApiTest extends AbstractBaseApiTest assertNotNull(pathElements.get(4).getId()); } + /** + * Tests well-known aliases. + *

GET:

+ *
    + *
  • {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-}
  • + *
  • {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-}
  • + *
+ */ @Test public void testGetNodeWithKnownAlias() throws Exception { @@ -472,9 +514,170 @@ public class NodeApiTest extends AbstractBaseApiTest getSingle(NodesEntityResource.class, user1, userNodeAlias, null, 404); // Not found } - private String getChildrenUrl(NodeRef nodeRef) + /** + * Tests Multipart upload to user's home (a.k.a My Files). + *

POST:

+ * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes//children} + */ + @Test + public void testUploadToMyFiles() throws Exception { - return "nodes/" + nodeRef.getId() + "/children"; + final String userNodeAlias = "-my-"; + final String fileName = "quick.pdf"; + final File file = getResourceFile(fileName); + + Paging paging = getPaging(0, Integer.MAX_VALUE); + HttpResponse response = getAll(getChildrenUrl(userNodeAlias), user1, paging, 200); + PublicApiClient.ExpectedPaging pagingResult = parsePaging(response.getJsonResponse()); + assertNotNull(paging); + final int numOfNodes = pagingResult.getCount().intValue(); + + MultiPartBuilder multiPartBuilder = MultiPartBuilder.create() + .setFileData(new FileData(fileName, file, MimetypeMap.MIMETYPE_PDF)); + MultiPartRequest reqBody = multiPartBuilder.build(); + + // Try to upload + response = post(getChildrenUrl(userNodeAlias), user1, new String(reqBody.getBody()), null, reqBody.getContentType(), 201); + Document document = jacksonUtil.parseEntry(response.getJsonResponse(), Document.class); + // Check the upload response + assertEquals(fileName, document.getName()); + ContentInfo contentInfo = document.getContent(); + assertNotNull(contentInfo); + assertEquals(MimetypeMap.MIMETYPE_PDF, contentInfo.getMimeType()); + + // Retrieve the uploaded file + response = getSingle(NodesEntityResource.class, user1, document.getNodeRef().getId(), null, 200); + document = jacksonUtil.parseEntry(response.getJsonResponse(), Document.class); + assertEquals(fileName, document.getName()); + contentInfo = document.getContent(); + assertNotNull(contentInfo); + assertEquals(MimetypeMap.MIMETYPE_PDF, contentInfo.getMimeType()); + + // Check 'get children' is confirming the upload + response = getAll(getChildrenUrl(userNodeAlias), user1, paging, 200); + pagingResult = parsePaging(response.getJsonResponse()); + assertNotNull(paging); + assertEquals(numOfNodes + 1, pagingResult.getCount().intValue()); + + // Upload the same file again to check the name conflicts handling + post(getChildrenUrl(userNodeAlias), user1, new String(reqBody.getBody()), null, reqBody.getContentType(), 409); + + response = getAll(getChildrenUrl(userNodeAlias), user1, paging, 200); + pagingResult = parsePaging(response.getJsonResponse()); + assertNotNull(paging); + assertEquals("Duplicate file name. The file shouldn't have been uploaded.", numOfNodes + 1, pagingResult.getCount().intValue()); + + // User2 tries to upload a new file into the user1's home folder. + response = getSingle(NodesEntityResource.class, user1, userNodeAlias, null, 200); + Folder user1Home = jacksonUtil.parseEntry(response.getJsonResponse(), Folder.class); + final String fileName2 = "quick-2.txt"; + final File file2 = getResourceFile(fileName2); + reqBody = MultiPartBuilder.create() + .setFileData(new FileData(fileName2, file2, MimetypeMap.MIMETYPE_TEXT_PLAIN)) + .build(); + post(getChildrenUrl(user1Home.getNodeRef()), user2, new String(reqBody.getBody()), null, reqBody.getContentType(), 403); + + response = getAll(getChildrenUrl(userNodeAlias), user1, paging, 200); + pagingResult = parsePaging(response.getJsonResponse()); + assertNotNull(paging); + assertEquals("Access Denied. The file shouldn't have been uploaded.", numOfNodes + 1, pagingResult.getCount().intValue()); + + // User1 tries to upload a file into a document rather than a folder! + post(getChildrenUrl(document.getNodeRef()), user1, new String(reqBody.getBody()), null, reqBody.getContentType(), 400); + + // Try to upload a file without defining the required formData + reqBody = MultiPartBuilder.create().build(); + post(getChildrenUrl(userNodeAlias), user1, new String(reqBody.getBody()), null, reqBody.getContentType(), 400); + } + + /** + * Tests Multipart upload to a Site. + *

POST:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/nodes//children} + */ + @Test + public void testUploadToSite() throws Exception + { + final String fileName = "quick-1.txt"; + final File file = getResourceFile(fileName); + + AuthenticationUtil.setFullyAuthenticatedUser(userOneN1.getId()); + String folderA = "folder" + System.currentTimeMillis() + "_A"; + NodeRef folderA_Ref = repoService.addToDocumentLibrary(userOneN1Site, folderA, ContentModel.TYPE_FOLDER); + + Paging paging = getPaging(0, Integer.MAX_VALUE); + HttpResponse response = getAll(getChildrenUrl(folderA_Ref), userOneN1.getId(), paging, 200); + PublicApiClient.ExpectedPaging pagingResult = parsePaging(response.getJsonResponse()); + assertNotNull(paging); + final int numOfNodes = pagingResult.getCount().intValue(); + + MultiPartBuilder multiPartBuilder = MultiPartBuilder.create() + .setFileData(new FileData(fileName, file, MimetypeMap.MIMETYPE_TEXT_PLAIN)); + MultiPartRequest reqBody = multiPartBuilder.build(); + // Try to upload + response = post(getChildrenUrl(folderA_Ref), userOneN1.getId(), new String(reqBody.getBody()), null, reqBody.getContentType(), 201); + Document document = jacksonUtil.parseEntry(response.getJsonResponse(), Document.class); + // Check the upload response + assertEquals(fileName, document.getName()); + ContentInfo contentInfo = document.getContent(); + assertNotNull(contentInfo); + assertEquals(MimetypeMap.MIMETYPE_TEXT_PLAIN, contentInfo.getMimeType()); + + // Retrieve the uploaded file + response = getSingle(NodesEntityResource.class, userOneN1.getId(), document.getNodeRef().getId(), null, 200); + document = jacksonUtil.parseEntry(response.getJsonResponse(), Document.class); + assertEquals(fileName, document.getName()); + contentInfo = document.getContent(); + assertNotNull(contentInfo); + assertEquals(MimetypeMap.MIMETYPE_TEXT_PLAIN, contentInfo.getMimeType()); + + // Check 'get children' is confirming the upload + response = getAll(getChildrenUrl(folderA_Ref), userOneN1.getId(), paging, 200); + pagingResult = parsePaging(response.getJsonResponse()); + assertNotNull(paging); + assertEquals(numOfNodes + 1, pagingResult.getCount().intValue()); + + // Upload the same file again to check the name conflicts handling + post(getChildrenUrl(folderA_Ref), userOneN1.getId(), new String(reqBody.getBody()), null, reqBody.getContentType(), 409); + + // Set overwrite=true and upload the same file again + reqBody = MultiPartBuilder.copy(multiPartBuilder) + .setOverwrite(true) + .build(); + post(getChildrenUrl(folderA_Ref), userOneN1.getId(), new String(reqBody.getBody()), null, reqBody.getContentType(), 201); + + response = getAll(getChildrenUrl(folderA_Ref), userOneN1.getId(), paging, 200); + pagingResult = parsePaging(response.getJsonResponse()); + assertNotNull(paging); + assertEquals(numOfNodes + 1, pagingResult.getCount().intValue()); + + final String fileName2 = "quick-2.txt"; + final File file2 = getResourceFile(fileName2); + reqBody = MultiPartBuilder.create() + .setFileData(new FileData(fileName2, file2, MimetypeMap.MIMETYPE_TEXT_PLAIN)) + .build(); + // userTwoN1 tries to upload a new file into the folderA of userOneN1 + post(getChildrenUrl(folderA_Ref), userTwoN1.getId(), new String(reqBody.getBody()), null, reqBody.getContentType(), 403); + } + + private String getChildrenUrl(NodeRef parentNodeRef) + { + return getChildrenUrl(parentNodeRef.getId()); + } + + private String getChildrenUrl(String parentId) + { + return "nodes/" + parentId + "/children"; + } + + private File getResourceFile(String fileName) throws FileNotFoundException + { + URL url = NodeApiTest.class.getClassLoader().getResource(RESOURCE_PREFIX + fileName); + if (url == null) + { + fail("Cannot get the resource: " + fileName); + } + return ResourceUtils.getFile(url); } @Override diff --git a/source/test-java/org/alfresco/rest/api/tests/util/MultiPartBuilder.java b/source/test-java/org/alfresco/rest/api/tests/util/MultiPartBuilder.java index 118ca967aa..2e97181728 100644 --- a/source/test-java/org/alfresco/rest/api/tests/util/MultiPartBuilder.java +++ b/source/test-java/org/alfresco/rest/api/tests/util/MultiPartBuilder.java @@ -57,7 +57,7 @@ public class MultiPartBuilder private String contentTypeQNameStr; private List aspects; private boolean majorVersion; - private boolean overwrite = true; // If a fileName clashes for a versionable file + private boolean overwrite = false; // If a fileName clashes for a versionable file private MultiPartBuilder() { @@ -232,11 +232,13 @@ public class MultiPartBuilder public MultiPartRequest build() throws IOException { - assertNotNull(fileData); List parts = new ArrayList<>(); - parts.add(new FilePart("filedata", fileData.getFileName(), fileData.getFile(), fileData.getMimetype(), null)); - addPartIfNotNull(parts, "filename", fileData.getFileName()); + if (fileData != null) + { + parts.add(new FilePart("filedata", fileData.getFileName(), fileData.getFile(), fileData.getMimetype(), null)); + addPartIfNotNull(parts, "filename", fileData.getFileName()); + } addPartIfNotNull(parts, "siteid", siteId); addPartIfNotNull(parts, "containerid", containerId); addPartIfNotNull(parts, "destination", destination); diff --git a/source/test-resources/publicapi/upload/quick-1.txt b/source/test-resources/publicapi/upload/quick-1.txt new file mode 100644 index 0000000000..ff3bb63948 --- /dev/null +++ b/source/test-resources/publicapi/upload/quick-1.txt @@ -0,0 +1 @@ +The quick brown fox jumps over the lazy dog \ No newline at end of file diff --git a/source/test-resources/publicapi/upload/quick-2.txt b/source/test-resources/publicapi/upload/quick-2.txt new file mode 100644 index 0000000000..ca103eec21 --- /dev/null +++ b/source/test-resources/publicapi/upload/quick-2.txt @@ -0,0 +1,2 @@ +Gym class featuring a brown fox and lazy dog +The quick brown fox jumps over the lazy dog \ No newline at end of file diff --git a/source/test-resources/publicapi/upload/quick.pdf b/source/test-resources/publicapi/upload/quick.pdf new file mode 100644 index 0000000000..a1779afd8b Binary files /dev/null and b/source/test-resources/publicapi/upload/quick.pdf differ