/*
|
http-base - Extensions to jdk.httpserver
|
Copyright (C) 2021 Ulrich Hilger
|
|
This program is free software: you can redistribute it and/or modify
|
it under the terms of the GNU Affero General Public License as
|
published by the Free Software Foundation, either version 3 of the
|
License, or (at your option) any later version.
|
|
This program is distributed in the hope that it will be useful,
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
GNU Affero General Public License for more details.
|
|
You should have received a copy of the GNU Affero General Public License
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*/
|
package de.uhilger.httpserver.base.actor;
|
|
import com.sun.net.httpserver.Headers;
|
import com.sun.net.httpserver.HttpExchange;
|
import de.uhilger.httpserver.base.Range;
|
import de.uhilger.httpserver.base.RangeGroup;
|
import static de.uhilger.httpserver.base.handler.FileHandler.CONTENT_RANGE_HEADER;
|
import static de.uhilger.httpserver.base.handler.FileHandler.RANGE_HEADER;
|
import static de.uhilger.httpserver.base.handler.FileHandler.RANGE_PATTERN;
|
import static de.uhilger.httpserver.base.handler.FileHandler.SC_PARTIAL_CONTENT;
|
import static de.uhilger.httpserver.base.handler.FileHandler.STR_BLANK;
|
import static de.uhilger.httpserver.base.handler.FileHandler.STR_COMMA;
|
import static de.uhilger.httpserver.base.handler.FileHandler.STR_DASH;
|
import static de.uhilger.httpserver.base.handler.FileHandler.STR_SLASH;
|
import de.uhilger.httpserver.base.HttpResponder;
|
import java.io.File;
|
import java.io.FileInputStream;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.io.OutputStream;
|
import java.util.Iterator;
|
|
/**
|
*
|
* @author Ulrich Hilger
|
* @version 1, 11.06.2021
|
*/
|
public class FileActor {
|
|
/**
|
* Einen Teil des Inhalts einer Datei ausliefern
|
*
|
* Wenn eine Range angefragt wird, hat die Antwort einen Content-Range Header
|
* wie folgt:
|
*
|
* <code>
|
* Content-Range: bytes 0-1023/146515
|
* Content-Length: 1024
|
* </code>
|
*
|
* Wenn mehrere Ranges angefragt werden, hat die Antwort mehrere Content-Range
|
* Header als Multipart Response. Multipart Responses fehlen dieser
|
* Implementierung noch.
|
*
|
* (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
|
*
|
* @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
|
* Anfertigen und Senden der Antwort
|
* @param file die Datei, deren Inhalt teilweise ausgeliefert werden soll
|
* @throws IOException falls etwas schief geht entsteht dieser Fehler
|
*/
|
/*
|
*/
|
public void serveFileParts(HttpExchange e, File file) throws IOException {
|
if (file.exists()) {
|
HttpResponder fs = new HttpResponder();
|
fs.setHeaders(e, file);
|
long responseLength = 0;
|
long start = 0;
|
long end;
|
RangeGroup rangeGroup = parseRanges(e, file);
|
Iterator<Range> i = rangeGroup.getRanges();
|
Headers resHeaders = e.getResponseHeaders();
|
while (i.hasNext()) {
|
Range range = i.next();
|
start = range.getStart();
|
end = range.getEnd();
|
resHeaders.add(CONTENT_RANGE_HEADER, contentRangeHdr(range, file));
|
responseLength += (end - start);
|
}
|
e.sendResponseHeaders(SC_PARTIAL_CONTENT, responseLength);
|
if(HttpResponder.HTTP_GET.equalsIgnoreCase(e.getRequestMethod())) {
|
InputStream is = new FileInputStream(file);
|
OutputStream os = e.getResponseBody();
|
if (start > 0) {
|
is.skip(start);
|
}
|
long count = 0;
|
int byteRead = is.read();
|
while (byteRead > -1 && count < responseLength) {
|
++count;
|
os.write(byteRead);
|
byteRead = is.read();
|
}
|
os.flush();
|
os.close();
|
is.close();
|
}
|
} else {
|
HttpResponder fs = new HttpResponder();
|
fs.sendNotFound(e, file.getName());
|
}
|
}
|
|
/**
|
* Die Byte-Ranges aus dem Range-Header ermitteln.
|
*
|
* Der Range-Header kann unterschiedliche Abschnitte bezeichnen, Beispiele:
|
* Range: bytes=200-1000, 2000-6576, 19000- Range: bytes=0-499, -500 (vgl.
|
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)
|
*
|
* @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
|
* Anfertigen und Senden der Antwort
|
* @param file die Datei, deren Inhalt ausgeliefert werden soll
|
* @return die angefragten Byte-Ranges
|
*/
|
protected RangeGroup parseRanges(HttpExchange e, File file) {
|
RangeGroup ranges = new RangeGroup();
|
String rangeHeader = e.getRequestHeaders().get(RANGE_HEADER).toString();
|
|
/*
|
Inhalt des Range-Headers von nicht benoetigten Angaben befreien
|
|
Ein Range Header enthaelt neben den Start- und Endwerten der Ranges auch
|
die Angabe "bytes:". Es ist aber keine andere Auspraegung als Bytes
|
spezifiziert, daher muss die Angabe nicht ausgewertet werden und kann
|
entfallen. Der Range-Header kann zudem noch eckige Klammern haben
|
wie in [bytes=200-1000].
|
|
Der regulaere Ausdruck "[^\\d-,]" bezeichnet alle Zeichen, die keine
|
Ziffern 0-9, Bindestrich oder Komma sind.
|
*/
|
rangeHeader = rangeHeader.replaceAll(RANGE_PATTERN, "");
|
|
/*
|
Die Ranges ermitteln.
|
|
Nach dem vorangegangenen Schritt besteht der Header-Ausdruck nur noch
|
aus einer mit Kommas getrennten Liste aus Start- und Endwerten wie z.B.
|
"-103,214-930,1647-"
|
|
Ein Range-Ausdruck kann dann drei verschiedene Auspraegungen haben:
|
1. Startwert fehlt, z.B. -200
|
2. Start und Ende sind vorhanden, z.B. 101-200
|
3. Endwert fehlt, z.B. 201-
|
|
Teilt man einen Range-String mit der Methode String.split("-") am
|
Bindestrich ('-') in ein String-Array 'values' gilt:
|
values.length < 2: Fall 3 ist gegeben
|
values.length > 1 und values[0].length < 1: Fall 1 ist gegeben
|
ansonsten: Fall 2 ist gegeben
|
*/
|
String[] rangeArray = rangeHeader.split(STR_COMMA);
|
for (String rangeStr : rangeArray) {
|
Range range = new Range();
|
String[] values = rangeStr.split(STR_DASH);
|
if (values.length < 2) {
|
// Fall 3
|
range.setStart(Long.parseLong(values[0]));
|
range.setEnd(file.length());
|
} else {
|
if (values[0].length() < 1) {
|
// Fall 1
|
range.setStart(0);
|
range.setEnd(Long.parseLong(values[1]));
|
} else {
|
// Fall 2
|
range.setStart(Long.parseLong(values[0]));
|
range.setEnd(Long.parseLong(values[1]));
|
}
|
}
|
ranges.addRange(range);
|
}
|
return ranges;
|
}
|
|
/**
|
* Einen Content-Range Header erzeugen
|
*
|
* @param range die Range, aus deren Inhalt der Header erzeugt werden soll
|
* @param file die Datei, die den Inhalt liefert, der vom Header
|
* bezeichnet wird
|
* @return der Inhalt des Content-Range Headers
|
*/
|
protected String contentRangeHdr(Range range, File file) {
|
StringBuilder sb = new StringBuilder();
|
sb.append(HttpResponder.STR_BYTES);
|
sb.append(STR_BLANK);
|
sb.append(range.getStart());
|
sb.append(STR_DASH);
|
sb.append(range.getEnd());
|
sb.append(STR_SLASH);
|
sb.append(file.length());
|
return sb.toString();
|
}
|
|
|
|
|
}
|