Skip to content

Commit 425108f

Browse files
das7padtimja
andauthored
Add new streaming mode for LargeText (#722)
Co-authored-by: Tim Jacomb <[email protected]>
1 parent 4b60534 commit 425108f

File tree

2 files changed

+457
-2
lines changed

2 files changed

+457
-2
lines changed

core/src/main/java/org/kohsuke/stapler/framework/io/LargeText.java

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@
4040
import java.nio.charset.Charset;
4141
import java.nio.file.Files;
4242
import java.nio.file.StandardOpenOption;
43+
import java.util.UUID;
4344
import java.util.zip.GZIPInputStream;
45+
import net.sf.json.JSONObject;
4446
import org.apache.commons.io.output.CountingOutputStream;
4547
import org.kohsuke.stapler.ReflectionUtils;
48+
import org.kohsuke.stapler.Stapler;
4649
import org.kohsuke.stapler.StaplerRequest;
4750
import org.kohsuke.stapler.StaplerRequest2;
4851
import org.kohsuke.stapler.StaplerResponse;
@@ -84,12 +87,19 @@ public interface Source {
8487
boolean exists();
8588
}
8689

90+
/**
91+
* For multipart/form-data streaming mode: Enable searching for the first new line character after the provided ?start and abort searching at the given position.
92+
*/
93+
protected static final String SEARCH_STOP_PARAMETER = "searchNewLineUntil";
94+
8795
private final Source source;
8896

8997
protected final Charset charset;
9098

9199
private volatile boolean completed;
92100

101+
private JSONObject streamingMeta;
102+
93103
public LargeText(File file, boolean completed) {
94104
this(file, Charset.defaultCharset(), completed);
95105
}
@@ -257,13 +267,24 @@ public long writeLogTo(long start, OutputStream out) throws IOException {
257267
}
258268

259269
private void writeLogUncounted(long start, OutputStream os) throws IOException {
260-
261270
try (Session f = source.open()) {
262271
if (f.skip(start) != start) {
263272
throw new EOFException("Attempted to read past the end of the log");
264273
}
265274

266-
if (completed) {
275+
if (isStreamingRequest(Stapler.getCurrentRequest2())) {
276+
// write everything until the previously determined EOF in bulk
277+
long end = source.length();
278+
long pos = start;
279+
byte[] buf = new byte[64 * 1024];
280+
int n;
281+
while (pos < end && (n = f.read(buf)) >= 0) {
282+
long remaining = end - pos;
283+
if (n > remaining) n = (int) remaining;
284+
os.write(buf, 0, n);
285+
pos += n;
286+
}
287+
} else if (completed) {
267288
// write everything till EOF
268289
byte[] buf = new byte[1024];
269290
int sz;
@@ -310,7 +331,111 @@ public void doProgressText(StaplerRequest req, StaplerResponse rsp) throws IOExc
310331
doProgressTextImpl(StaplerRequest.toStaplerRequest2(req), StaplerResponse.toStaplerResponse2(rsp));
311332
}
312333

334+
/**
335+
* Detect use of streaming mode.
336+
* @param req The current request.
337+
* @return true if the new streaming mode is requested.
338+
*/
339+
public boolean isStreamingRequest(StaplerRequest2 req) {
340+
if (req == null) return false;
341+
String accept = req.getHeader("Accept");
342+
if (accept == null || accept.isEmpty()) return false;
343+
return accept.startsWith("multipart/form-data");
344+
}
345+
346+
/**
347+
* Add additional meta data to a streaming response.
348+
* @param key The field to (over)write meta data for.
349+
* @param value The meta data value.
350+
*/
351+
protected void putStreamingMeta(String key, Object value) {
352+
if (streamingMeta == null) streamingMeta = new JSONObject();
353+
streamingMeta.put(key, value);
354+
}
355+
356+
private long findNextLineStart(long start, long stop) throws IOException {
357+
try (var f = source.open()) {
358+
if (f.skip(start) != start) {
359+
// The log file rolled over, send it in full.
360+
return 0;
361+
}
362+
byte[] buf = new byte[64 * 1024];
363+
long searchPosition = start;
364+
int n;
365+
while (searchPosition + 1 < stop && (n = f.read(buf)) > 0) {
366+
for (int i = 0; i < n && searchPosition + 1 < stop; i++) {
367+
if (buf[i] == '\n') {
368+
// We have a match, send from after the \n.
369+
putStreamingMeta("startFromNewLine", true);
370+
return searchPosition + 1;
371+
}
372+
searchPosition++;
373+
}
374+
}
375+
}
376+
// fall back to original start.
377+
return start;
378+
}
379+
380+
private void doProgressTextStreaming(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
381+
setContentType(rsp);
382+
String contentType = rsp.getContentType();
383+
if (contentType == null || contentType.isEmpty()) {
384+
contentType = "text/plain;charset=UTF-8";
385+
}
386+
if (contentType.contains("\r") || contentType.contains("\n")) {
387+
throw new IOException("Found CR/LF in Content-Type. Aborting streaming mode");
388+
}
389+
String boundary = UUID.randomUUID().toString();
390+
rsp.setContentType("multipart/form-data;boundary=" + boundary);
391+
rsp.setStatus(HttpServletResponse.SC_OK);
392+
putStreamingMeta("completed", completed);
393+
394+
try (var writer = rsp.getWriter()) {
395+
writer.write("--" + boundary + "\r\n" // preamble for first part
396+
+ "Content-Disposition: form-data;name=text\r\n"
397+
+ "Content-Type: " + contentType + "\r\n"
398+
+ "\r\n");
399+
long length = source.exists() ? source.length() : 0;
400+
401+
String s = req.getParameter("start");
402+
long start = (s != null) ? Long.parseLong(s) : 0;
403+
if (start > length) {
404+
// text rolled over, send in full
405+
start = 0;
406+
} else if (start < 0 && length <= -start) {
407+
// tail on small file, send in full
408+
start = 0;
409+
} else if (start < 0) {
410+
// tail on large file, start at first new line in tail
411+
start = findNextLineStart(length + start, length);
412+
} else if (req.getParameter(SEARCH_STOP_PARAMETER) != null) {
413+
// fetch more, start at first new line before last start
414+
long searchStop = Long.parseLong(req.getParameter(SEARCH_STOP_PARAMETER));
415+
start = findNextLineStart(start, searchStop);
416+
}
417+
putStreamingMeta("start", start);
418+
419+
long end = length;
420+
if (start != end) {
421+
end = writeLogTo(start, writer);
422+
}
423+
putStreamingMeta("end", end);
424+
425+
writer.write("\r\n--" + boundary + "\r\n" // transition to next part
426+
+ "Content-Disposition: form-data;name=meta\r\n"
427+
+ "Content-Type: application/json;charset=utf-8\r\n"
428+
+ "\r\n"
429+
+ streamingMeta + "\r\n--" + boundary + "--");
430+
}
431+
}
432+
313433
private void doProgressTextImpl(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException {
434+
if (isStreamingRequest(req)) {
435+
doProgressTextStreaming(req, rsp);
436+
return;
437+
}
438+
314439
setContentType(rsp);
315440
rsp.setStatus(HttpServletResponse.SC_OK);
316441

0 commit comments

Comments
 (0)