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 0000000000..f455a92b47 Binary files /dev/null and b/scm-webapp/src/test/resources/JustBytes differ diff --git a/scm-webapp/src/test/resources/SomeGoCode.go b/scm-webapp/src/test/resources/SomeGoCode.go new file mode 100644 index 0000000000..18d6395a74 --- /dev/null +++ b/scm-webapp/src/test/resources/SomeGoCode.go @@ -0,0 +1 @@ +package resources