From 815cee156933a7747f4ea4c0ff26e6d4939c138e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 13 Aug 2018 15:52:50 +0200 Subject: [PATCH] Return content as stream --- .../scm/api/v2/resources/ContentResource.java | 50 ++++--- .../api/v2/resources/ContentResourceTest.java | 131 ++++++++++++++++++ scm-webapp/src/test/resources/Dockerfile | 8 ++ scm-webapp/src/test/resources/JustBytes | Bin 0 -> 1024 bytes scm-webapp/src/test/resources/SomeGoCode.go | 1 + 5 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java create mode 100644 scm-webapp/src/test/resources/Dockerfile create mode 100644 scm-webapp/src/test/resources/JustBytes create mode 100644 scm-webapp/src/test/resources/SomeGoCode.go diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java index 9d15f14f50..06acec1409 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java @@ -2,7 +2,8 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.spotter.ContentType; import com.github.sdorra.spotter.ContentTypes; -import com.github.sdorra.spotter.Language; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.PathNotFoundException; import sonia.scm.repository.RepositoryException; @@ -16,12 +17,14 @@ import javax.ws.rs.HEAD; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.Optional; public class ContentResource { + private static final Logger LOG = LoggerFactory.getLogger(ContentResource.class); + private final RepositoryServiceFactory servicefactory; @Inject @@ -34,17 +37,27 @@ public class ContentResource { public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) { try (RepositoryService repositoryService = servicefactory.create(new NamespaceAndName(namespace, name))) { try { - byte[] content = getContent(revision, path, repositoryService); - Response.ResponseBuilder responseBuilder = Response.ok(content); - appendContentType(path, content, responseBuilder); + + StreamingOutput stream = os -> { + try { + repositoryService.getCatCommand().setRevision(revision).retriveContent(os, path); + } catch (RepositoryException e) { + e.printStackTrace(); + } + os.close(); + }; + + Response.ResponseBuilder responseBuilder = Response.ok(stream); + appendContentType(path, getHead(revision, path, repositoryService), responseBuilder); + return responseBuilder.build(); } catch (PathNotFoundException e) { return Response.status(404).build(); } catch (IOException e) { - e.printStackTrace(); + LOG.error("error reading repository resource {} from {}/{}", path, namespace, name, e); return Response.status(500).entity(e.getMessage()).build(); } catch (RepositoryException e) { - e.printStackTrace(); + LOG.error("error reading repository resource {} from {}/{}", path, namespace, name, e); return Response.status(500).entity(e.getMessage()).build(); } } catch (RepositoryNotFoundException e) { @@ -57,19 +70,18 @@ public class ContentResource { public Response metadata(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) { try (RepositoryService repositoryService = servicefactory.create(new NamespaceAndName(namespace, name))) { try { - byte[] content = getContent(revision, path, repositoryService); Response.ResponseBuilder responseBuilder = Response.ok(); - appendContentType(path, content, responseBuilder); + appendContentType(path, getHead(revision, path, repositoryService), responseBuilder); return responseBuilder.build(); } catch (PathNotFoundException e) { return Response.status(404).build(); } catch (IOException e) { - e.printStackTrace(); + LOG.error("error reading repository resource {} from {}/{}", path, namespace, name, e); return Response.status(500).entity(e.getMessage()).build(); } catch (RepositoryException e) { - e.printStackTrace(); + LOG.error("error reading repository resource {} from {}/{}", path, namespace, name, e); return Response.status(500).entity(e.getMessage()).build(); } } catch (RepositoryNotFoundException e) { @@ -77,18 +89,14 @@ public class ContentResource { } } - private void appendContentType(String path, byte[] content, Response.ResponseBuilder responseBuilder) { - ContentType contentType = ContentTypes.detect(path, content); - System.out.println("Content-Type: " + contentType); - - Optional language = contentType.getLanguage(); - if (language.isPresent()) { - responseBuilder.header("Content-Type", contentType); - } - responseBuilder.header("Content-Length", content.length); + private void appendContentType(String path, byte[] head, Response.ResponseBuilder responseBuilder) { + ContentType contentType = ContentTypes.detect(path, head); + responseBuilder.header("Content-Type", contentType.getRaw()); + contentType.getLanguage().ifPresent(language -> responseBuilder.header("Language", language)); + responseBuilder.header("Content-Length", head.length); } - private byte[] getContent(String revision, String path, RepositoryService repositoryService) throws IOException, RepositoryException { + private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException, RepositoryException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); repositoryService.getCatCommand().setRevision(revision).retriveContent(outputStream, path); return outputStream.toByteArray(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java new file mode 100644 index 0000000000..0493ef5289 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java @@ -0,0 +1,131 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.io.Resources; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.PathNotFoundException; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.api.CatCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.AdditionalMatchers.not; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ContentResourceTest { + + private static final String NAMESPACE = "space"; + private static final String REPO_NAME = "name"; + private static final String REV = "rev"; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private RepositoryServiceFactory repositoryServiceFactory; + + @InjectMocks + private ContentResource contentResource; + + private CatCommandBuilder catCommand; + + @Before + public void initService() throws Exception { + NamespaceAndName existingNamespaceAndName = new NamespaceAndName(NAMESPACE, REPO_NAME); + RepositoryService repositoryService = repositoryServiceFactory.create(existingNamespaceAndName); + catCommand = repositoryService.getCatCommand(); + when(catCommand.setRevision(REV)).thenReturn(catCommand); + + // defaults for unknown things + doThrow(new RepositoryNotFoundException("x")).when(repositoryServiceFactory).create(not(eq(existingNamespaceAndName))); + doThrow(new PathNotFoundException("x")).when(catCommand).retriveContent(any(), any()); + } + + @Test + public void shouldReadSimpleFile() throws Exception { + mockContent("file", "Hello".getBytes()); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file"); + assertEquals(200, response.getStatus()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ((StreamingOutput) response.getEntity()).write(baos); + + assertEquals("Hello", baos.toString()); + } + + @Test + public void shouldHandleMissingFile() { + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "doesNotExist"); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldHandleMissingRepository() { + Response response = contentResource.get("no", "repo", REV, "anything"); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldRecognizeTikaSourceCode() throws Exception { + mockContentFromResource("SomeGoCode.go"); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go"); + assertEquals(200, response.getStatus()); + + assertEquals("GO", response.getHeaderString("Language")); + assertEquals("text/x-go", response.getHeaderString("Content-Type")); + } + + @Test + public void shouldRecognizeSpecialSourceCode() throws Exception { + mockContentFromResource("Dockerfile"); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile"); + assertEquals(200, response.getStatus()); + + assertEquals("DOCKERFILE", response.getHeaderString("Language")); + assertEquals("text/plain", response.getHeaderString("Content-Type")); + } + + @Test + public void shouldRandomByteFile() throws Exception { + mockContentFromResource("JustBytes"); + + Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "JustBytes"); + assertEquals(200, response.getStatus()); + + assertFalse(response.getHeaders().containsKey("Language")); + assertEquals("application/octet-stream", response.getHeaderString("Content-Type")); + } + + private void mockContentFromResource(String fileName) throws Exception { + URL url = Resources.getResource(fileName); + mockContent(fileName, Resources.toByteArray(url)); + } + + private void mockContent(String path, byte[] content) throws Exception { + doAnswer(invocation -> { + OutputStream outputStream = (OutputStream) invocation.getArguments()[0]; + outputStream.write(content); + outputStream.close(); + return null; + }).when(catCommand).retriveContent(any(), eq(path)); + } +} diff --git a/scm-webapp/src/test/resources/Dockerfile b/scm-webapp/src/test/resources/Dockerfile new file mode 100644 index 0000000000..d4b48dfd45 --- /dev/null +++ b/scm-webapp/src/test/resources/Dockerfile @@ -0,0 +1,8 @@ +FROM ubuntu + +COPY nothing /nowhere + +RUN apt-get update + +EXEC shutdhown -h now + diff --git a/scm-webapp/src/test/resources/JustBytes b/scm-webapp/src/test/resources/JustBytes new file mode 100644 index 0000000000000000000000000000000000000000..f455a92b4710106feb2670ffef3d213ea01a5958 GIT binary patch literal 1024 zcmV+b1poWV;w+C0=6yeghc-S^vF^!TEtD=Y13BL(*yv5Z!`(7r z+>&z#<>A(;$E&wSEqsx2>%`XkXQo?U=d7v7Fpag(zz+1l;|e29ZJkUh5Q?M#a~y?A zSm%VxBJhvmoOCDqspgwr)R3`L*xqAA(`|xpuyNIpA$aKE_}Q5~a!v?)gOv-R zKH-%0!R?fqv6F&pL|f-%b;;sH@oXg)Rf;PW_?nc^(uEhH2j|#@u3o#R7V_iQx7*=EGt2JaV1|TbG*r@%EcNUKIv`rFGR;Ib>G7WuK3941i>(V%PqRvMb zb{x0#mTfkc(8$AOXGhq=wd8I)yvG)cm|C>O0)e&r@&L47A&GE`3^x3-rJ^DNTp&+# z=s?FFfwFJ>hdEqBy70to{N;40Es1ZjjX6s5KA_G|nX!Uk2aJGXvy;8Aqs`{U`6|`D z4J^#p&d+u*VE}&?Ya}bJ&3o&jNg@n!9PSV=-Mh1Ur~&5iLQRM=YpI{6+H(3<>%|0b>f&G z+S_1QH53r3jzT{Q`iz1WVkfiOI$LmmBW8X{OsaM=hZnsQB_&IP3dwlQh7v=5_(h4| zLnSC6Pf4FiM%tvVV-=j_BAtZjN;ki~ES>9K=(6jj0ft-Pi~yZl3*g}7OW@Xe*y25j zget~FiTwwHsI7wELm?@jyHJSbiOrpC2n8YGd5ISj*JhF zQjkP)MoommIoqGvKS>2@&XM1*^D+ME|3OO??a4AL^VfjaEhgs08oEiBempOr%fsW; zf}voCvmfGaw`vf`c63aVG2Sb#GBB1_VEkm)gtmhb0a1jF1smA)&zxqA=n1jc-0Kh= uY