Skip to content

Commit 8b92b7c

Browse files
committed
Added static file handler
1 parent 2a43598 commit 8b92b7c

File tree

13 files changed

+127
-35
lines changed

13 files changed

+127
-35
lines changed

src/main/java/dev/latvian/apps/tinyserver/HTTPServer.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ private void handleClient(Socket socket) {
185185

186186
var method = firstLine.length == 2 ? HTTPMethod.fromString(firstLine[0]) : null;
187187
boolean writeBody = method != null && method.body();
188+
var originalMethod = method;
188189

189190
if (method == HTTPMethod.HEAD) {
190191
method = HTTPMethod.GET;
@@ -285,7 +286,7 @@ private void handleClient(Socket socket) {
285286

286287
var builder = createBuilder(req, null);
287288
builder.setStatus(HTTPStatus.NO_CONTENT);
288-
builder.setHeader("Allow", allowed.stream().map(HTTPMethod::name).collect(Collectors.joining(",")));
289+
builder.addHeader("Allow", allowed.stream().map(HTTPMethod::name).collect(Collectors.joining(",")));
289290
out = new BufferedOutputStream(socket.getOutputStream(), bufferSize);
290291
builder.write(out, writeBody);
291292
out.flush();
@@ -300,7 +301,7 @@ private void handleClient(Socket socket) {
300301
var handler = rootHandlers.get(method);
301302

302303
if (handler != null) {
303-
req.init(this, startTime, "", new String[0], CompiledPath.EMPTY, headers, queryString, query, in);
304+
req.init(this, originalMethod, startTime, "", new String[0], CompiledPath.EMPTY, headers, queryString, query, in);
304305
builder = createBuilder(req, handler.handler());
305306
}
306307
} else {
@@ -322,14 +323,14 @@ private void handleClient(Socket socket) {
322323
var h = hl.staticHandlers().get(path);
323324

324325
if (h != null) {
325-
req.init(this, startTime, path, pathParts, h.path(), headers, queryString, query, in);
326+
req.init(this, originalMethod, startTime, path, pathParts, h.path(), headers, queryString, query, in);
326327
builder = createBuilder(req, h.handler());
327328
} else {
328329
for (var dynamicHandler : hl.dynamicHandlers()) {
329330
var matches = dynamicHandler.path().matches(pathParts);
330331

331332
if (matches != null) {
332-
req.init(this, startTime, path, matches, dynamicHandler.path(), headers, queryString, query, in);
333+
req.init(this, originalMethod, startTime, path, matches, dynamicHandler.path(), headers, queryString, query, in);
333334
builder = createBuilder(req, dynamicHandler.handler());
334335
break;
335336
}
@@ -384,26 +385,26 @@ private void handleClient(Socket socket) {
384385
}
385386

386387
public HTTPPayload createBuilder(REQ req, @Nullable HTTPHandler<REQ> handler) {
387-
var builder = new HTTPPayload();
388+
var payload = new HTTPPayload();
388389

389390
if (serverName != null && !serverName.isEmpty()) {
390-
builder.setHeader("Server", serverName);
391+
payload.addHeader("Server", serverName);
391392
}
392393

393-
builder.setHeader("Date", HTTPPayload.DATE_TIME_FORMATTER.format(Instant.now()));
394+
payload.addHeader("Date", HTTPPayload.DATE_TIME_FORMATTER.format(Instant.now()));
394395

395396
if (handler != null) {
396397
try {
397398
var response = handler.handle(req);
398-
req.beforeResponse(builder, response);
399-
builder.setResponse(response);
400-
req.afterResponse(builder, response);
399+
req.beforeResponse(payload, response);
400+
payload.setResponse(response);
401+
req.afterResponse(payload, response);
401402
} catch (Exception ex) {
402-
builder.setStatus(HTTPStatus.INTERNAL_ERROR);
403-
req.handlePayloadError(builder, ex);
403+
payload.setStatus(HTTPStatus.INTERNAL_ERROR);
404+
req.handlePayloadError(payload, ex);
404405
}
405406
}
406407

407-
return builder;
408+
return payload;
408409
}
409410
}

src/main/java/dev/latvian/apps/tinyserver/ServerRegistry.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
import dev.latvian.apps.tinyserver.http.HTTPHandler;
44
import dev.latvian.apps.tinyserver.http.HTTPMethod;
55
import dev.latvian.apps.tinyserver.http.HTTPRequest;
6+
import dev.latvian.apps.tinyserver.http.PathFileHandler;
7+
import dev.latvian.apps.tinyserver.http.RootPathFileHandler;
68
import dev.latvian.apps.tinyserver.http.response.HTTPResponse;
79
import dev.latvian.apps.tinyserver.ws.WSHandler;
810
import dev.latvian.apps.tinyserver.ws.WSSession;
911
import dev.latvian.apps.tinyserver.ws.WSSessionFactory;
1012

13+
import java.nio.file.Path;
14+
import java.time.Duration;
1115
import java.util.function.Consumer;
1216

1317
public interface ServerRegistry<REQ extends HTTPRequest> {
@@ -52,6 +56,14 @@ default void redirect(String path, String redirect) {
5256
get(path, req -> res);
5357
}
5458

59+
default void files(String path, Path directory, Duration cacheDuration, boolean autoInedx) {
60+
if (autoInedx) {
61+
get(path, new RootPathFileHandler<>(path, directory));
62+
}
63+
64+
get(path + "/<path>", new PathFileHandler<>(path, directory, cacheDuration, autoInedx));
65+
}
66+
5567
<WSS extends WSSession<REQ>> WSHandler<REQ, WSS> ws(String path, WSSessionFactory<REQ, WSS> factory);
5668

5769
default <WSS extends WSSession<REQ>> WSHandler<REQ, WSS> ws(String path) {

src/main/java/dev/latvian/apps/tinyserver/http/HTTPRequest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
public class HTTPRequest {
2020
private HTTPServer<?> server;
21+
private HTTPMethod method;
2122
private long startTime = 0L;
2223
private String path = "";
2324
private String[] pathParts = new String[0];
@@ -30,8 +31,9 @@ public class HTTPRequest {
3031
private Map<String, String> formData = null;
3132

3233
@ApiStatus.Internal
33-
public void init(HTTPServer<?> server, long startTime, String path, String[] pathParts, CompiledPath compiledPath, List<Header> headers, String queryString, Map<String, String> query, InputStream bodyStream) {
34+
public void init(HTTPServer<?> server, HTTPMethod method, long startTime, String path, String[] pathParts, CompiledPath compiledPath, List<Header> headers, String queryString, Map<String, String> query, InputStream bodyStream) {
3435
this.server = server;
36+
this.method = method;
3537
this.startTime = startTime;
3638
this.path = path;
3739
this.pathParts = pathParts;
@@ -62,6 +64,10 @@ public HTTPServer<?> server() {
6264
return server;
6365
}
6466

67+
public HTTPMethod method() {
68+
return method;
69+
}
70+
6571
public long startTime() {
6672
return startTime;
6773
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package dev.latvian.apps.tinyserver.http;
2+
3+
import dev.latvian.apps.tinyserver.content.MimeType;
4+
import dev.latvian.apps.tinyserver.http.response.HTTPResponse;
5+
import dev.latvian.apps.tinyserver.http.response.HTTPStatus;
6+
7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.time.Duration;
12+
13+
public record PathFileHandler<REQ extends HTTPRequest>(String httpPath, Path directory, Duration cacheDuration, boolean autoInedx) implements HTTPHandler<REQ> {
14+
@Override
15+
public HTTPResponse handle(REQ req) throws IOException {
16+
var path = directory.resolve(req.variable("path"));
17+
18+
if (path.startsWith(directory) && Files.exists(path) && Files.isReadable(path)) {
19+
if (Files.isRegularFile(path)) {
20+
return HTTPResponse.ok().publicCache(cacheDuration).content(path);
21+
} else if (autoInedx && Files.isDirectory(path)) {
22+
return index(httpPath, directory, path);
23+
}
24+
}
25+
26+
return HTTPStatus.NOT_FOUND;
27+
}
28+
29+
public static HTTPResponse index(String httpPath, Path rootDirectory, Path directory) throws IOException {
30+
var sb = new StringBuilder();
31+
sb.append("<ul>");
32+
33+
if (!rootDirectory.equals(directory)) {
34+
sb.append("<li><a href=\"" + httpPath + "/" + rootDirectory.relativize(directory.getParent()) + "\">..</a></li>");
35+
}
36+
37+
for (var file : Files.list(directory).sorted().toList()) {
38+
var name = file.getFileName().toString();
39+
sb.append("<li><a href=\"" + httpPath + "/" + rootDirectory.relativize(file) + "\">" + name + "</a></li>");
40+
}
41+
42+
sb.append("</ul>");
43+
44+
return HTTPResponse.ok().content(sb.toString().getBytes(StandardCharsets.UTF_8), MimeType.HTML);
45+
}
46+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package dev.latvian.apps.tinyserver.http;
2+
3+
import dev.latvian.apps.tinyserver.http.response.HTTPResponse;
4+
import dev.latvian.apps.tinyserver.http.response.HTTPStatus;
5+
6+
import java.io.IOException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
10+
public record RootPathFileHandler<REQ extends HTTPRequest>(String httpPath, Path directory) implements HTTPHandler<REQ> {
11+
@Override
12+
public HTTPResponse handle(REQ req) throws IOException {
13+
if (Files.exists(directory) && Files.isDirectory(directory) && Files.isReadable(directory)) {
14+
return PathFileHandler.index(httpPath, directory, directory);
15+
}
16+
17+
return HTTPStatus.NOT_FOUND;
18+
}
19+
}

src/main/java/dev/latvian/apps/tinyserver/http/response/HTTPResponse.java

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,28 +63,20 @@ default HTTPResponse cookie(String key, String value, Consumer<HTTPResponseWithC
6363
return new HTTPResponseWithCookie(this, key, value, builder);
6464
}
6565

66-
default HTTPResponse cache(boolean isPublic, int seconds) {
67-
return new HTTPResponseWithCacheControl(this, isPublic, seconds);
66+
default HTTPResponse cache(boolean isPublic, Duration duration) {
67+
return new HTTPResponseWithCacheControl(this, isPublic, duration);
6868
}
6969

7070
default HTTPResponse noCache() {
71-
return cache(true, 0);
72-
}
73-
74-
default HTTPResponse publicCache(int seconds) {
75-
return cache(true, seconds);
71+
return cache(true, Duration.ZERO);
7672
}
7773

7874
default HTTPResponse publicCache(Duration duration) {
79-
return publicCache((int) duration.toSeconds());
80-
}
81-
82-
default HTTPResponse privateCache(int seconds) {
83-
return cache(false, seconds);
75+
return cache(true, duration);
8476
}
8577

8678
default HTTPResponse privateCache(Duration duration) {
87-
return privateCache((int) duration.toSeconds());
79+
return cache(false, duration);
8880
}
8981

9082
default HTTPResponse content(ResponseContent content) {

src/main/java/dev/latvian/apps/tinyserver/http/response/HTTPResponseWithCacheControl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package dev.latvian.apps.tinyserver.http.response;
22

3+
import java.time.Duration;
4+
35
public record HTTPResponseWithCacheControl(HTTPResponse original, String value) implements HTTPResponse {
4-
public HTTPResponseWithCacheControl(HTTPResponse original, boolean isPublic, int seconds) {
5-
this(original, seconds <= 0 ? "no-cache, no-store, must-revalidate, max-age=0" : ((isPublic ? "public, max-age=" : "private, max-age=") + seconds));
6+
public HTTPResponseWithCacheControl(HTTPResponse original, boolean isPublic, Duration duration) {
7+
this(original, duration.isPositive() ? ((isPublic ? "public, max-age=" : "private, max-age=") + duration.toSeconds()) : "no-cache, no-store, must-revalidate, max-age=0");
68
}
79

810
@Override

src/main/java/dev/latvian/apps/tinyserver/http/response/HTTPResponseWithHeader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
public record HTTPResponseWithHeader(HTTPResponse original, String header, String value) implements HTTPResponse {
44
@Override
55
public void build(HTTPPayload payload) throws Exception {
6-
payload.setHeader(header, value);
6+
payload.addHeader(header, value);
77
original.build(payload);
88
}
99
}

src/main/java/dev/latvian/apps/tinyserver/ws/WSResponse.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ public record WSResponse(WSSession<?> session, byte[] accept) implements HTTPRes
1010
@Override
1111
public void build(HTTPPayload payload) {
1212
payload.setStatus(HTTPStatus.SWITCHING_PROTOCOLS);
13-
payload.setHeader("Upgrade", "websocket");
14-
payload.setHeader("Connection", "Upgrade");
15-
payload.setHeader("Sec-WebSocket-Accept", Base64.getEncoder().encodeToString(accept));
13+
payload.addHeader("Upgrade", "websocket");
14+
payload.addHeader("Connection", "Upgrade");
15+
payload.addHeader("Sec-WebSocket-Accept", Base64.getEncoder().encodeToString(accept));
1616
}
1717
}

src/test/java/dev/latvian/apps/tinyserver/test/TestRequest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import dev.latvian.apps.tinyserver.http.response.HTTPPayload;
55
import dev.latvian.apps.tinyserver.http.response.HTTPResponse;
66

7+
import java.nio.file.Path;
8+
79
public class TestRequest extends HTTPRequest {
810
@Override
911
public void beforeResponse(HTTPPayload payload, HTTPResponse response) {
@@ -12,10 +14,15 @@ public void beforeResponse(HTTPPayload payload, HTTPResponse response) {
1214

1315
@Override
1416
public void afterResponse(HTTPPayload payload, HTTPResponse response) {
15-
System.out.println("/" + fullPath() + " " + payload.getStatus() + ", " + (System.currentTimeMillis() - startTime()) + " ms");
17+
System.out.println(method() + " /" + fullPath() + " " + payload.getStatus() + ", " + (System.currentTimeMillis() - startTime()) + " ms");
1618
System.out.println("- Cookies: " + cookies());
1719
System.out.println("- Headers: " + headers());
1820
System.out.println("- Query: " + query());
19-
System.out.println("- Form: " + form());
21+
22+
if (!header("Content-Length").isEmpty()) {
23+
System.out.println("- Form: " + formData());
24+
}
25+
26+
System.out.println(Path.of("src/test/resources").toAbsolutePath());
2027
}
2128
}

0 commit comments

Comments
 (0)