aboutsummaryrefslogtreecommitdiffstats
path: root/src/server/HttpServer.hx
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/HttpServer.hx')
-rw-r--r--src/server/HttpServer.hx214
1 files changed, 174 insertions, 40 deletions
diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx
index 7622708..6602e86 100644
--- a/src/server/HttpServer.hx
+++ b/src/server/HttpServer.hx
@@ -1,8 +1,10 @@
package server;
+import Types.UploadResponse;
+import haxe.Json;
import haxe.io.Path;
import js.node.Buffer;
-import js.node.Fs;
+import js.node.Fs.Fs;
import js.node.Http;
import js.node.Https;
import js.node.Path as JsPath;
@@ -12,6 +14,14 @@ import js.node.http.ServerResponse;
import js.node.url.URL;
import sys.FileSystem;
+@:structInit
+private class HttpServerConfig {
+ public final dir:String;
+ public final customDir:String = null;
+ public final allowLocalRequests = false;
+ public final cache:Cache = null;
+}
+
class HttpServer {
static final mimeTypes = [
"html" => "text/html",
@@ -36,31 +46,49 @@ class HttpServer {
"wasm" => "application/wasm"
];
- static var dir:String;
- static var customDir:String;
- static var hasCustomRes = false;
- static var allowedLocalFiles:Map<String, Bool> = [];
- static var allowLocalRequests = false;
- static final CHUNK_SIZE = 1024 * 1024 * 5; // 5 MB
+ final main:Main;
+ final dir:String;
+ final customDir:String;
+ final hasCustomRes = false;
+ final allowedLocalFiles:Map<String, Bool> = [];
+ final allowLocalRequests = false;
+ final cache:Cache = null;
+ final CHUNK_SIZE = 1024 * 1024 * 5; // 5 MB
+ final uploadingFiles:Map<String, Int> = [];
+ final uploadingFilesLastChunks:Map<String, Buffer> = [];
+
+ public function new(main:Main, config:HttpServerConfig):Void {
+ this.main = main;
+ dir = config.dir;
+ customDir = config.customDir;
+ allowLocalRequests = config.allowLocalRequests;
+ cache = config.cache;
- public static function init(dir:String, ?customDir:String, allowLocalRequests:Bool):Void {
- HttpServer.dir = dir;
- if (customDir == null) return;
- HttpServer.customDir = customDir;
- hasCustomRes = FileSystem.exists(customDir);
- HttpServer.allowLocalRequests = allowLocalRequests;
+ if (customDir != null) hasCustomRes = FileSystem.exists(customDir);
}
- public static function serveFiles(req:IncomingMessage, res:ServerResponse):Void {
+ public function serveFiles(req:IncomingMessage, res:ServerResponse):Void {
final url = try {
new URL(safeDecodeURI(req.url), "http://localhost");
- } catch (e) new URL("/", "http://localhost");
+ } catch (e) {
+ new URL("/", "http://localhost");
+ }
var filePath = getPath(dir, url);
final ext = Path.extension(filePath).toLowerCase();
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("Content-Type", getMimeType(ext));
+ if (cache != null && req.method == "POST") {
+ switch url.pathname {
+ case "/upload-last-chunk":
+ uploadFileLastChunk(req, res);
+ case "/upload":
+ uploadFile(req, res);
+ }
+ return;
+ }
+
if (allowLocalRequests && req.socket.remoteAddress == req.socket.localAddress
|| allowedLocalFiles[url.pathname]) {
if (isMediaExtension(ext)) {
@@ -103,14 +131,104 @@ class HttpServer {
});
}
- static function getPath(dir:String, url:URL):String {
+ function uploadFileLastChunk(req:IncomingMessage, res:ServerResponse) {
+ final name = cache.getFreeFileName(req.headers["content-name"]);
+ final filePath = cache.getFilePath(name);
+ final body:Array<Any> = [];
+ req.on("data", chunk -> body.push(chunk));
+ req.on("end", () -> {
+ final buffer = Buffer.concat(body);
+ uploadingFilesLastChunks[filePath] = buffer;
+ res.writeHead(200, {
+ 'Content-Type': 'application/json',
+ });
+ final json:UploadResponse = {
+ info: "File last chunk uploaded"
+ }
+ res.end(Json.stringify(json));
+ });
+ }
+
+ function uploadFile(req:IncomingMessage, res:ServerResponse) {
+ final name = cache.getFreeFileName(req.headers["content-name"]);
+ final clientName = req.headers["client-name"];
+ final filePath = cache.getFilePath(name);
+ final size = Std.parseInt(req.headers["content-length"]) ?? return;
+ var written = 0;
+
+ inline function end(json:UploadResponse):Void {
+ uploadingFiles.remove(name);
+ uploadingFilesLastChunks.remove(name);
+
+ res.statusCode = 200;
+ res.end(Json.stringify(json));
+ }
+
+ if (cache.getFreeSpace() < size) {
+ end({
+ info: "Error: Not enough free space on server or file size is out of cache storage limit.",
+ errorId: "freeSpace"
+ });
+ return;
+ }
+
+ final stream = Fs.createWriteStream(filePath);
+ req.pipe(stream);
+
+ inline function onStart() {
+ cache.removeOlderCache(size);
+ cache.add(name);
+ uploadingFiles[filePath] = size;
+ }
+ var isStart = true;
+ req.on("data", chunk -> {
+ var url:String = null;
+ if (isStart) {
+ isStart = false;
+ onStart();
+ url = cache.getFileUrl(name);
+ }
+ written += chunk.length;
+ final ratio = (written / size).clamp(0, 1);
+ final percent = (ratio * 100).toFixed(2);
+ final client = main.clients.getByName(clientName) ?? return;
+ main.send(client, {
+ type: Progress,
+ progress: {
+ type: Uploading,
+ ratio: ratio,
+ data: url
+ }
+ });
+ });
+ stream.on("close", () -> {
+ end({
+ info: "File write stream closed.",
+ });
+ });
+ stream.on("error", err -> {
+ trace(err);
+ end({
+ info: "File write stream error.",
+ });
+ });
+ req.on("error", err -> {
+ trace("Request Error:", err);
+ stream.destroy();
+ end({
+ info: "File request error.",
+ });
+ });
+ }
+
+ function getPath(dir:String, url:URL):String {
var filePath = dir + url.pathname;
filePath = filePath.urlDecode();
if (!FileSystem.isDirectory(filePath)) return filePath;
return Path.addTrailingSlash(filePath) + "index.html";
}
- static function readFileError(err:Dynamic, res:ServerResponse, filePath:String):Void {
+ function readFileError(err:Dynamic, res:ServerResponse, filePath:String):Void {
res.setHeader("Content-Type", getMimeType("html"));
if (err.code == "ENOENT") {
res.statusCode = 404;
@@ -122,9 +240,13 @@ class HttpServer {
}
}
- static function serveMedia(req:IncomingMessage, res:ServerResponse, filePath:String):Bool {
+ function serveMedia(req:IncomingMessage, res:ServerResponse, filePath:String):Bool {
if (!Fs.existsSync(filePath)) return false;
- final videoSize = Fs.statSync(filePath).size;
+ var videoSize:Int = cast Fs.statSync(filePath).size;
+ // use future content length to start playing it before uploaded
+ if (uploadingFiles.exists(filePath)) {
+ videoSize = uploadingFiles[filePath];
+ }
final rangeHeader:String = req.headers["range"];
if (rangeHeader == null) {
res.statusCode = 200;
@@ -142,23 +264,33 @@ class HttpServer {
res.setHeader("Content-Range", 'bytes $start-$end/$videoSize');
res.setHeader("Content-Length", '$contentLength');
- // HTTP Status 206 for Partial Content
- res.statusCode = 206;
- // create video read stream for this particular chunk
- final videoStream = Fs.createReadStream(filePath, {start: cast start, end: cast end});
+ res.statusCode = 206; // partial content
+
+ // check for last chunk cache for instant play while uploading
+ final buffer = uploadingFilesLastChunks[filePath];
+ if (buffer != null && end == videoSize - 1 && contentLength < buffer.byteLength) {
+ final bufferStart = (buffer.byteLength - contentLength).limitMin(0);
+ res.end(buffer.slice(bufferStart));
+ return true;
+ }
+
// stream the video chunk to the client
+ final videoStream = Fs.createReadStream(
+ filePath,
+ {start: start, end: end}
+ );
videoStream.pipe(res);
res.on("error", () -> videoStream.destroy());
res.on("close", () -> videoStream.destroy());
return true;
}
- static function parseRangeHeader(rangeHeader:String, videoSize:Float):{start:Float, end:Float} {
+ function parseRangeHeader(rangeHeader:String, videoSize:Int):{start:Int, end:Int} {
final ranges = ~/[-=]/g.split(rangeHeader);
- var start = Std.parseFloat(ranges[1]);
+ var start = Std.parseInt(ranges[1]);
if (Utils.isOutOfRange(start, 0, videoSize - 1)) start = 0;
- var end = Std.parseFloat(ranges[2]);
- if (Math.isNaN(end)) end = start + CHUNK_SIZE;
+ var end = Std.parseInt(ranges[2]);
+ if (end == null) end = start + CHUNK_SIZE;
if (Utils.isOutOfRange(end, start, videoSize - 1)) end = videoSize - 1;
return {
start: start,
@@ -166,14 +298,14 @@ class HttpServer {
};
}
- static function isMediaExtension(ext:String):Bool {
+ function isMediaExtension(ext:String):Bool {
return ext == "mp4" || ext == "webm" || ext == "mp3" || ext == "wav";
}
- static final matchLang = ~/^[A-z]+/;
- static final matchVarString = ~/\${([A-z_]+)}/g;
+ final matchLang = ~/^[A-z]+/;
+ final matchVarString = ~/\${([A-z_]+)}/g;
- static function localizeHtml(data:String, lang:String):String {
+ function localizeHtml(data:String, lang:String):String {
if (lang != null && matchLang.match(lang)) {
lang = matchLang.matched(0);
} else lang = "en";
@@ -184,7 +316,7 @@ class HttpServer {
return data;
}
- static function proxyUrl(req:IncomingMessage, res:ServerResponse):Bool {
+ function proxyUrl(req:IncomingMessage, res:ServerResponse):Bool {
final url = req.url.replace("/proxy?url=", "");
final proxy = proxyRequest(url, req, res, proxyRes -> {
final url = proxyRes.headers["location"] ?? return false;
@@ -201,7 +333,7 @@ class HttpServer {
return true;
}
- static function proxyRequest(
+ function proxyRequest(
url:String,
req:IncomingMessage,
res:ServerResponse,
@@ -209,7 +341,9 @@ class HttpServer {
):Null<ClientRequest> {
final url = try {
new URL(safeDecodeURI(url));
- } catch (e) return null;
+ } catch (e) {
+ return null;
+ }
if (url.host == req.headers["host"]) return null;
final options = {
host: url.hostname,
@@ -222,7 +356,7 @@ class HttpServer {
final request = url.protocol == "https:" ? Https.request : Http.request;
final proxy = request(options, proxyRes -> {
if (cancelProxyRequest(proxyRes)) return;
- proxyRes.headers["Content-Type"] = "application/octet-stream";
+ proxyRes.headers["content-type"] = "application/octet-stream";
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
});
@@ -232,18 +366,18 @@ class HttpServer {
return proxy;
}
- static function isChildOf(parent:String, child:String):Bool {
+ function isChildOf(parent:String, child:String):Bool {
final rel = JsPath.relative(parent, child);
return rel.length > 0 && !rel.startsWith('..') && !JsPath.isAbsolute(rel);
}
- static function getMimeType(ext:String):String {
+ function getMimeType(ext:String):String {
return mimeTypes[ext] ?? return "application/octet-stream";
}
- static final ctrlCharacters = ~/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/g;
+ final ctrlCharacters = ~/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/g;
- static function safeDecodeURI(data:String):String {
+ function safeDecodeURI(data:String):String {
try {
data = decodeURI(data);
} catch (err) {
@@ -253,7 +387,7 @@ class HttpServer {
return data;
}
- static inline function decodeURI(data:String):String {
+ inline function decodeURI(data:String):String {
return js.Syntax.code("decodeURI({0})", data);
}
}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage