Skip to content

Commit eef0edf

Browse files
committed
Added static file handlers, better part parsing, better error handling
1 parent 7522dac commit eef0edf

26 files changed

+318
-139
lines changed

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

+23-11
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.jetbrains.annotations.Nullable;
44

5-
public record CompiledPath(Part[] parts, int variables, boolean wildcard) {
6-
public static final CompiledPath EMPTY = new CompiledPath(new Part[0], 0, false);
5+
import java.util.ArrayList;
6+
7+
public record CompiledPath(Part[] parts, String string, int variables, boolean wildcard) {
8+
public static final CompiledPath EMPTY = new CompiledPath(new Part[0], "", 0, false);
79

810
public record Part(String name, boolean variable) {
911
public boolean matches(String string) {
@@ -14,6 +16,8 @@ public boolean matches(String string) {
1416
public static CompiledPath compile(String string) {
1517
var ostring = string;
1618

19+
string = string.trim().replace('\\', '/');
20+
1721
while (string.startsWith("/")) {
1822
string = string.substring(1);
1923
}
@@ -27,30 +31,33 @@ public static CompiledPath compile(String string) {
2731
}
2832

2933
var partsStr = string.split("/");
30-
var parts = new Part[partsStr.length];
34+
var parts = new ArrayList<Part>(partsStr.length);
35+
var toString = new ArrayList<String>();
3136
boolean wildcard = false;
3237
int variables = 0;
3338

34-
for (int i = 0; i < partsStr.length; i++) {
35-
var s = partsStr[i];
36-
37-
if (wildcard) {
39+
for (var s : partsStr) {
40+
if (s.isEmpty()) {
41+
continue;
42+
} else if (wildcard) {
3843
throw new IllegalArgumentException("<wildcard> argument must be the last part of the path '" + ostring + "'");
3944
}
4045

4146
if (s.startsWith("{") && s.endsWith("}")) {
42-
parts[i] = new Part(s.substring(1, s.length() - 1), true);
47+
parts.add(new Part(s.substring(1, s.length() - 1), true));
4348
variables++;
4449
} else if (s.startsWith("<") && s.endsWith(">")) {
45-
parts[i] = new Part(s.substring(1, s.length() - 1), true);
50+
parts.add(new Part(s.substring(1, s.length() - 1), true));
4651
variables++;
4752
wildcard = true;
4853
} else {
49-
parts[i] = new Part(s, false);
54+
parts.add(new Part(s, false));
5055
}
56+
57+
toString.add(s);
5158
}
5259

53-
return new CompiledPath(parts, variables, wildcard);
60+
return parts.isEmpty() ? EMPTY : new CompiledPath(parts.toArray(new Part[0]), String.join("/", toString), variables, wildcard);
5461
}
5562

5663
@Nullable
@@ -90,4 +97,9 @@ public String[] matches(String[] path) {
9097

9198
return null;
9299
}
100+
101+
@Override
102+
public String toString() {
103+
return string;
104+
}
93105
}

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

+15-15
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
import dev.latvian.apps.tinyserver.http.response.HTTPPayload;
1212
import dev.latvian.apps.tinyserver.http.response.HTTPResponse;
1313
import dev.latvian.apps.tinyserver.http.response.HTTPStatus;
14-
import dev.latvian.apps.tinyserver.ws.WSEndpointHandler;
15-
import dev.latvian.apps.tinyserver.ws.WSHandler;
16-
import dev.latvian.apps.tinyserver.ws.WSSession;
17-
import dev.latvian.apps.tinyserver.ws.WSSessionFactory;
14+
import dev.latvian.apps.tinyserver.http.response.error.HTTPError;
1815
import org.jetbrains.annotations.Nullable;
1916

2017
import java.io.IOException;
@@ -35,9 +32,9 @@
3532
import java.util.IdentityHashMap;
3633
import java.util.Map;
3734
import java.util.Set;
38-
import java.util.concurrent.ConcurrentHashMap;
3935
import java.util.function.Supplier;
4036
import java.util.stream.Collectors;
37+
import java.util.stream.Stream;
4138

4239
public class HTTPServer<REQ extends HTTPRequest> implements Runnable, ServerRegistry<REQ> {
4340
private static final Object DUMMY = new Object();
@@ -169,13 +166,6 @@ public void http(HTTPMethod method, String path, HTTPHandler<REQ> handler) {
169166
}
170167
}
171168

172-
@Override
173-
public <WSS extends WSSession<REQ>> WSHandler<REQ, WSS> ws(String path, WSSessionFactory<REQ, WSS> factory) {
174-
var handler = new WSEndpointHandler<>(factory, new ConcurrentHashMap<>(), daemon);
175-
get(path, handler);
176-
return handler;
177-
}
178-
179169
@Override
180170
public final void run() {
181171
serverStarted();
@@ -407,7 +397,6 @@ boolean handleClient(HTTPConnection<REQ> connection) {
407397

408398
if (builder == null) {
409399
builder = createBuilder(req, null);
410-
builder.setStatus(HTTPStatus.NOT_FOUND);
411400
}
412401

413402
builder.process(req, keepAliveTimeout, keepAlive ? maxKeepAliveConnections : 0);
@@ -429,6 +418,12 @@ boolean handleClient(HTTPConnection<REQ> connection) {
429418

430419
public HTTPPayload createBuilder(REQ req, @Nullable HTTPHandler<REQ> handler) {
431420
var payload = new HTTPPayload(serverName, Instant.now());
421+
var preResponse = req.createPreResponse(handler);
422+
423+
if (preResponse != null) {
424+
payload.setResponse(preResponse);
425+
return payload;
426+
}
432427

433428
if (handler != null) {
434429
HTTPResponse response;
@@ -438,12 +433,13 @@ public HTTPPayload createBuilder(REQ req, @Nullable HTTPHandler<REQ> handler) {
438433
response = handler.handle(req);
439434
error = null;
440435
} catch (Throwable error1) {
441-
response = HTTPStatus.INTERNAL_ERROR;
436+
response = error1 instanceof HTTPError h ? h.getStatus() : HTTPStatus.INTERNAL_ERROR;
442437
error = error1;
443438
}
444439

445-
payload.setStatus(response.status());
446440
payload.setResponse(req.handleResponse(payload, response, error));
441+
} else {
442+
payload.setResponse(HTTPStatus.NOT_FOUND.defaultResponse());
447443
}
448444

449445
return payload;
@@ -466,4 +462,8 @@ protected void queueSession(HTTPConnection<REQ> session) {
466462
public Set<HTTPConnection<REQ>> connections() {
467463
return publicConnections;
468464
}
465+
466+
public Stream<HTTPPathHandler<REQ>> handlers() {
467+
return handlers.values().stream().flatMap(h -> Stream.concat(h.staticHandlers().values().stream(), h.dynamicHandlers().stream()));
468+
}
469469
}

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

+33-5
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
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;
6+
import dev.latvian.apps.tinyserver.http.file.DynamicFileHandler;
7+
import dev.latvian.apps.tinyserver.http.file.FileIndexHandler;
8+
import dev.latvian.apps.tinyserver.http.file.FileResponseHandler;
9+
import dev.latvian.apps.tinyserver.http.file.SingleFileHandler;
710
import dev.latvian.apps.tinyserver.http.response.HTTPResponse;
11+
import dev.latvian.apps.tinyserver.ws.WSEndpointHandler;
812
import dev.latvian.apps.tinyserver.ws.WSHandler;
913
import dev.latvian.apps.tinyserver.ws.WSSession;
1014
import dev.latvian.apps.tinyserver.ws.WSSessionFactory;
1115

16+
import java.io.IOException;
17+
import java.nio.file.Files;
1218
import java.nio.file.Path;
13-
import java.time.Duration;
19+
import java.util.concurrent.ConcurrentHashMap;
1420
import java.util.function.Consumer;
1521

1622
public interface ServerRegistry<REQ extends HTTPRequest> {
@@ -55,8 +61,12 @@ default void redirect(String path, String redirect) {
5561
get(path, req -> res);
5662
}
5763

58-
default void files(String path, Path directory, Duration cacheDuration, boolean autoIndex) {
59-
var handler = new PathFileHandler<REQ>(path, directory, cacheDuration, autoIndex);
64+
default void singleFile(String path, Path file, FileResponseHandler responseHandler) throws IOException {
65+
get(path, new SingleFileHandler<>(file, Files.probeContentType(file), responseHandler));
66+
}
67+
68+
default void dynamicFiles(String path, Path directory, FileResponseHandler responseHandler, boolean autoIndex) {
69+
var handler = new DynamicFileHandler<REQ>(directory, responseHandler, autoIndex);
6070

6171
if (autoIndex) {
6272
get(path, handler);
@@ -65,7 +75,25 @@ default void files(String path, Path directory, Duration cacheDuration, boolean
6575
get(path + "/<path>", handler);
6676
}
6777

68-
<WSS extends WSSession<REQ>> WSHandler<REQ, WSS> ws(String path, WSSessionFactory<REQ, WSS> factory);
78+
default void staticFiles(String path, Path directory, FileResponseHandler responseHandler, boolean autoIndex) throws IOException {
79+
path = path.endsWith("/") ? path : (path + "/");
80+
81+
for (var file : Files.walk(directory).filter(Files::isReadable).toList()) {
82+
var rpath = directory.relativize(file).toString();
83+
84+
if (Files.isRegularFile(file)) {
85+
singleFile(path + rpath, file, responseHandler);
86+
} else if (autoIndex && Files.isDirectory(file)) {
87+
get(path + rpath, new FileIndexHandler<>(directory, file, responseHandler));
88+
}
89+
}
90+
}
91+
92+
default <WSS extends WSSession<REQ>> WSHandler<REQ, WSS> ws(String path, WSSessionFactory<REQ, WSS> factory) {
93+
var handler = new WSEndpointHandler<>(factory, new ConcurrentHashMap<>());
94+
get(path, handler);
95+
return handler;
96+
}
6997

7098
default <WSS extends WSSession<REQ>> WSHandler<REQ, WSS> ws(String path) {
7199
return ws(path, (WSSessionFactory) WSSessionFactory.DEFAULT);

src/main/java/dev/latvian/apps/tinyserver/content/ByteContent.java

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import java.io.OutputStream;
55

66
public record ByteContent(byte[] bytes, String type) implements ResponseContent {
7+
public static final ByteContent EMPTY = new ByteContent(new byte[0], "");
8+
79
@Override
810
public long length() {
911
return bytes.length;
@@ -18,4 +20,9 @@ public void write(OutputStream out) throws IOException {
1820
public byte[] toBytes() {
1921
return bytes;
2022
}
23+
24+
@Override
25+
public boolean hasData() {
26+
return bytes.length > 0;
27+
}
2128
}

src/main/java/dev/latvian/apps/tinyserver/content/FileContent.java

+5
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,9 @@ public void transferTo(HTTPConnection<?> connection) throws IOException {
5353
}
5454
}
5555
}
56+
57+
@Override
58+
public boolean hasData() {
59+
return Files.exists(file) && Files.isRegularFile(file) && Files.isReadable(file);
60+
}
5661
}

src/main/java/dev/latvian/apps/tinyserver/content/ResponseContent.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ default String type() {
1717
return "";
1818
}
1919

20+
default boolean hasData() {
21+
return true;
22+
}
23+
2024
void write(OutputStream out) throws IOException;
2125

2226
default byte[] toBytes() throws IOException {
@@ -30,6 +34,7 @@ default void transferTo(HTTPConnection<?> connection) throws IOException {
3034
}
3135

3236
default HttpRequest.BodyPublisher bodyPublisher() throws IOException {
33-
return HttpRequest.BodyPublishers.ofByteArray(toBytes());
37+
var b = toBytes();
38+
return b.length == 0 ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofByteArray(b);
3439
}
3540
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@
55

66
public record HTTPPathHandler<REQ extends HTTPRequest>(HTTPMethod method, CompiledPath path, HTTPHandler<REQ> handler) {
77
public static final HTTPPathHandler<?> DEFAULT = new HTTPPathHandler<>(HTTPMethod.GET, CompiledPath.EMPTY, req -> HTTPStatus.OK);
8+
9+
@Override
10+
public String toString() {
11+
return method + " /" + path;
12+
}
813
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ public String userAgent() {
238238
return header("User-Agent");
239239
}
240240

241+
@Nullable
242+
public HTTPResponse createPreResponse(@Nullable HTTPHandler<?> handler) {
243+
return null;
244+
}
245+
241246
public HTTPResponse handleResponse(HTTPPayload payload, HTTPResponse response, @Nullable Throwable error) {
242247
if (error != null) {
243248
error.printStackTrace();

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

-55
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package dev.latvian.apps.tinyserver.http.file;
2+
3+
import dev.latvian.apps.tinyserver.http.HTTPHandler;
4+
import dev.latvian.apps.tinyserver.http.HTTPRequest;
5+
import dev.latvian.apps.tinyserver.http.response.HTTPResponse;
6+
import dev.latvian.apps.tinyserver.http.response.HTTPStatus;
7+
8+
import java.io.IOException;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
12+
public record DynamicFileHandler<REQ extends HTTPRequest>(Path directory, FileResponseHandler responseHandler, boolean autoIndex) implements HTTPHandler<REQ> {
13+
@Override
14+
public HTTPResponse handle(REQ req) throws IOException {
15+
var pathVar = req.variables().get("path");
16+
17+
if (pathVar == null) {
18+
if (autoIndex && Files.exists(directory) && Files.isDirectory(directory) && Files.isReadable(directory)) {
19+
return FileIndexHandler.index("/" + req.path(), directory, directory);
20+
}
21+
22+
return HTTPStatus.NOT_FOUND;
23+
}
24+
25+
var path = directory.resolve(pathVar);
26+
27+
if (path.startsWith(directory) && Files.exists(path) && Files.isReadable(path)) {
28+
if (Files.isRegularFile(path)) {
29+
return responseHandler.apply(HTTPResponse.ok().content(path), false, path);
30+
} else if (autoIndex && Files.isDirectory(path)) {
31+
return responseHandler.apply(FileIndexHandler.index("/" + req.path(), directory, path), true, path);
32+
}
33+
}
34+
35+
return HTTPStatus.NOT_FOUND;
36+
}
37+
}

0 commit comments

Comments
 (0)