mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-08 08:32:12 +01:00
Add line limits to content endpoint
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.spotter.ContentType;
|
||||
@@ -44,12 +44,14 @@ import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HEAD;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.core.StreamingOutput;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ContentResource {
|
||||
@@ -68,11 +70,12 @@ public class ContentResource {
|
||||
* Returns the content of a file for the given revision in the repository. The content type depends on the file
|
||||
* content and can be discovered calling <code>HEAD</code> on the same URL. If a programming languge could be
|
||||
* recognized, this will be given in the header <code>Language</code>.
|
||||
*
|
||||
* @param namespace the namespace of the repository
|
||||
* @param namespace the namespace of the repository
|
||||
* @param name the name of the repository
|
||||
* @param revision the revision
|
||||
* @param path The path of the file
|
||||
* @param start
|
||||
* @param end
|
||||
*/
|
||||
@GET
|
||||
@Path("{revision}/{path: .*}")
|
||||
@@ -94,8 +97,14 @@ public class ContentResource {
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) {
|
||||
StreamingOutput stream = createStreamingOutput(namespace, name, revision, path);
|
||||
public Response get(
|
||||
@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("revision") String revision,
|
||||
@PathParam("path") String path,
|
||||
@QueryParam("start") Integer start,
|
||||
@QueryParam("end") Integer end) {
|
||||
StreamingOutput stream = createStreamingOutput(namespace, name, revision, path, start, end);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
Response.ResponseBuilder responseBuilder = Response.ok(stream);
|
||||
return createContentHeader(namespace, name, revision, path, repositoryService, responseBuilder);
|
||||
@@ -105,11 +114,17 @@ public class ContentResource {
|
||||
}
|
||||
}
|
||||
|
||||
private StreamingOutput createStreamingOutput(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) {
|
||||
private StreamingOutput createStreamingOutput(String namespace, String name, String revision, String path, Integer start, Integer end) {
|
||||
return os -> {
|
||||
OutputStream sourceOut;
|
||||
if (start != null || end != null) {
|
||||
sourceOut = new LineFilteredOutputStream(os, start, end);
|
||||
} else {
|
||||
sourceOut = os;
|
||||
}
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
repositoryService.getCatCommand().setRevision(revision).retriveContent(os, path);
|
||||
os.close();
|
||||
repositoryService.getCatCommand().setRevision(revision).retriveContent(sourceOut, path);
|
||||
sourceOut.close();
|
||||
} catch (NotFoundException e) {
|
||||
LOG.debug(e.getMessage());
|
||||
throw new WebApplicationException(Status.NOT_FOUND);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
class LineFilteredOutputStream extends OutputStream {
|
||||
private final OutputStream target;
|
||||
private final int start;
|
||||
private final Integer end;
|
||||
|
||||
private boolean inLineBreak;
|
||||
private int currentLine = 0;
|
||||
|
||||
LineFilteredOutputStream(OutputStream target, Integer start, Integer end) {
|
||||
this.target = target;
|
||||
this.start = start == null ? 0 : start;
|
||||
this.end = end == null ? Integer.MAX_VALUE : end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
switch (b) {
|
||||
case '\n':
|
||||
case '\r':
|
||||
if (!inLineBreak) {
|
||||
inLineBreak = true;
|
||||
++currentLine;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (inLineBreak && currentLine > start && currentLine <= end) {
|
||||
target.write('\n');
|
||||
}
|
||||
inLineBreak = false;
|
||||
if (currentLine >= start && currentLine < end) {
|
||||
target.write(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (inLineBreak && currentLine >= start && currentLine < end) {
|
||||
target.write('\n');
|
||||
}
|
||||
target.close();
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
@@ -89,7 +89,7 @@ public class ContentResourceTest {
|
||||
public void shouldReadSimpleFile() throws Exception {
|
||||
mockContent("file", "Hello".getBytes());
|
||||
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file");
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file", null, null);
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
ByteArrayOutputStream baos = readOutputStream(response);
|
||||
@@ -97,15 +97,27 @@ public class ContentResourceTest {
|
||||
assertEquals("Hello", baos.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldLimitOutputByLines() throws Exception {
|
||||
mockContent("file", "line 1\nline 2\nline 3\nline 4".getBytes());
|
||||
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "file", 1, 3);
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
ByteArrayOutputStream baos = readOutputStream(response);
|
||||
|
||||
assertEquals("line 2\nline 3\n", baos.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleMissingFile() {
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "doesNotExist");
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "doesNotExist", null, null);
|
||||
assertEquals(404, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleMissingRepository() {
|
||||
Response response = contentResource.get("no", "repo", REV, "anything");
|
||||
Response response = contentResource.get("no", "repo", REV, "anything", null, null);
|
||||
assertEquals(404, response.getStatus());
|
||||
}
|
||||
|
||||
@@ -113,7 +125,7 @@ public class ContentResourceTest {
|
||||
public void shouldRecognizeTikaSourceCode() throws Exception {
|
||||
mockContentFromResource("SomeGoCode.go");
|
||||
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go");
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go", null, null);
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
assertEquals("golang", response.getHeaderString("X-Programming-Language"));
|
||||
@@ -124,7 +136,7 @@ public class ContentResourceTest {
|
||||
public void shouldRecognizeSpecialSourceCode() throws Exception {
|
||||
mockContentFromResource("Dockerfile");
|
||||
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile");
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile", null, null);
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
assertEquals("dockerfile", response.getHeaderString("X-Programming-Language"));
|
||||
@@ -135,7 +147,7 @@ public class ContentResourceTest {
|
||||
public void shouldHandleRandomByteFile() throws Exception {
|
||||
mockContentFromResource("JustBytes");
|
||||
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "JustBytes");
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "JustBytes", null, null);
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
assertFalse(response.getHeaders().containsKey("Language"));
|
||||
@@ -158,7 +170,7 @@ public class ContentResourceTest {
|
||||
public void shouldHandleEmptyFile() throws Exception {
|
||||
mockContent("empty", new byte[]{});
|
||||
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "empty");
|
||||
Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "empty", null, null);
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
assertFalse(response.getHeaders().containsKey("Language"));
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LineFilteredOutputStreamTest {
|
||||
|
||||
static final String INPUT_LF = "line 1\nline 2\nline 3\nline 4";
|
||||
static final String INPUT_CR_LF = "line 1\r\nline 2\r\nline 3\r\nline 4";
|
||||
static final String INPUT_CR = "line 1\rline 2\rline 3\rline 4";
|
||||
|
||||
ByteArrayOutputStream target = new ByteArrayOutputStream();
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {INPUT_LF, INPUT_CR_LF, INPUT_CR})
|
||||
void shouldNotFilterIfStartAndEndAreNotSet(String input) throws IOException {
|
||||
try (LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, null, null)) {
|
||||
filtered.write(input.getBytes());
|
||||
}
|
||||
|
||||
assertThat(target.toString()).isEqualTo(INPUT_LF);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {INPUT_LF, INPUT_CR_LF, INPUT_CR})
|
||||
void shouldNotFilterIfStartAndEndAreSetToLimits(String input) throws IOException {
|
||||
try (LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, 0, 4)) {
|
||||
filtered.write(input.getBytes());
|
||||
}
|
||||
|
||||
assertThat(target.toString()).isEqualTo(INPUT_LF);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {INPUT_LF, INPUT_CR_LF, INPUT_CR})
|
||||
void shouldRemoveFirstLinesIfStartIsSetGreaterThat1(String input) throws IOException {
|
||||
LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, 2, null);
|
||||
|
||||
filtered.write(input.getBytes());
|
||||
|
||||
assertThat(target.toString()).isEqualTo("line 3\nline 4");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {INPUT_LF, INPUT_CR_LF, INPUT_CR})
|
||||
void shouldOmitLastLinesIfEndIsSetLessThatLength(String input) throws IOException {
|
||||
LineFilteredOutputStream filtered = new LineFilteredOutputStream(target, null, 2);
|
||||
|
||||
filtered.write(input.getBytes());
|
||||
|
||||
assertThat(target.toString()).isEqualTo("line 1\nline 2\n");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user