Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ac88efa0c |
19
LICENSE
19
LICENSE
@ -1,19 +0,0 @@
|
|||||||
Copyright (c) 2025-2026 QPQ AG
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
BIN
etc/gh-telnet.png
Normal file
BIN
etc/gh-telnet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
BIN
etc/hello-world.png
Normal file
BIN
etc/hello-world.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
etc/observer.png
Normal file
BIN
etc/observer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
51
etc/pingpong.erl
Normal file
51
etc/pingpong.erl
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
-module(pingpong).
|
||||||
|
-export([main/0]).
|
||||||
|
|
||||||
|
% this runs two processes and has them talk to each other
|
||||||
|
% output:
|
||||||
|
%
|
||||||
|
% 2> pingpong:main().
|
||||||
|
% <0.96.0> received {pong, <0.97.0>, 10}
|
||||||
|
% <0.97.0> received {ping, <0.96.0>, 9}
|
||||||
|
% <0.96.0> received {pong, <0.97.0>, 8}
|
||||||
|
% <0.97.0> received {ping, <0.96.0>, 7}
|
||||||
|
% <0.96.0> received {pong, <0.97.0>, 6}
|
||||||
|
% <0.97.0> received {ping, <0.96.0>, 5}
|
||||||
|
% <0.96.0> received {pong, <0.97.0>, 4}
|
||||||
|
% <0.97.0> received {ping, <0.96.0>, 3}
|
||||||
|
% <0.96.0> received {pong, <0.97.0>, 2}
|
||||||
|
% <0.97.0> received {ping, <0.96.0>, 1}
|
||||||
|
% <0.96.0> received {pong, <0.97.0>, 0}
|
||||||
|
|
||||||
|
|
||||||
|
main() ->
|
||||||
|
PingerPID = spawn(fun pinger/0),
|
||||||
|
PongerPID = spawn(fun ponger/0),
|
||||||
|
PingerPID ! {pong, PongerPID, 10}.
|
||||||
|
|
||||||
|
pinger() ->
|
||||||
|
receive
|
||||||
|
{pong, PID, N} ->
|
||||||
|
io:format("~p received {pong, ~p, ~p}~n", [self(), PID, N]),
|
||||||
|
% ignore once we get to 0 or lower
|
||||||
|
case N > 0 of
|
||||||
|
true -> PID ! {ping, self(), N-1};
|
||||||
|
false -> ok
|
||||||
|
end,
|
||||||
|
% once we're done, go back to the top
|
||||||
|
pinger()
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
ponger() ->
|
||||||
|
receive
|
||||||
|
{ping, PID, N} ->
|
||||||
|
io:format("~p received {ping, ~p, ~p}~n", [self(), PID, N]),
|
||||||
|
% ignore once we get to 0 or lower
|
||||||
|
case N > 0 of
|
||||||
|
true -> PID ! {pong, self(), N-1};
|
||||||
|
false -> ok
|
||||||
|
end,
|
||||||
|
% once we're done, go back to the top
|
||||||
|
ponger()
|
||||||
|
end.
|
||||||
BIN
etc/v001-end.png
Normal file
BIN
etc/v001-end.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
BIN
etc/v001-start.png
Normal file
BIN
etc/v001-start.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
@ -1,26 +0,0 @@
|
|||||||
`gex_httpd`: Gajumaru Exchange HTTP Daemon
|
|
||||||
=====================================================================
|
|
||||||
|
|
||||||
GOALS
|
|
||||||
====================================================================
|
|
||||||
|
|
||||||
|
|
||||||
GOAL STACK
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
- use `gh_sfc` in `gh_client`
|
|
||||||
- don't spam filesystem for 404/500
|
|
||||||
|
|
||||||
|
|
||||||
TODONE
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
- ~~replace `io:format` calls with zx log~~
|
|
||||||
- ~~write out call paths for `gh_sfc`~~
|
|
||||||
|
|
||||||
|
|
||||||
GOAL QUEUE
|
|
||||||
--------------------------------------------------------------------
|
|
||||||
|
|
||||||
- mit license
|
|
||||||
- copyright/author bullshit in each module
|
|
||||||
@ -5,6 +5,5 @@
|
|||||||
{applications,[stdlib,kernel]},
|
{applications,[stdlib,kernel]},
|
||||||
{vsn,"0.1.0"},
|
{vsn,"0.1.0"},
|
||||||
{modules,[gex_httpd,gh_client,gh_client_man,gh_client_sup,
|
{modules,[gex_httpd,gh_client,gh_client_man,gh_client_sup,
|
||||||
gh_clients,gh_ct,gh_sfc,gh_sfc_cache,gh_sfc_entry,
|
gh_clients,gh_sup]},
|
||||||
gh_sup]},
|
|
||||||
{mod,{gex_httpd,[]}}]}.
|
{mod,{gex_httpd,[]}}]}.
|
||||||
|
|||||||
11
gex_httpd/priv/404.html
Normal file
11
gex_httpd/priv/404.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>QHL: 404</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>404 Not Found</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
11
gex_httpd/priv/500.html
Normal file
11
gex_httpd/priv/500.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>QHL: 500</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>500 Internal Server Error</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -3,15 +3,14 @@
|
|||||||
%%% @end
|
%%% @end
|
||||||
|
|
||||||
-module(gex_httpd).
|
-module(gex_httpd).
|
||||||
-behavior(application).
|
|
||||||
|
|
||||||
-vsn("0.1.0").
|
-vsn("0.1.0").
|
||||||
|
-behavior(application).
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-copyright("2025-2026 QPQ AG").
|
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
%% for our edification
|
%% for our edification
|
||||||
-export([listen/1, ignore/0]).
|
-export([listen/1, ignore/0]).
|
||||||
|
-export([start/0]).
|
||||||
|
|
||||||
%% erlang expects us to export these functions
|
%% erlang expects us to export these functions
|
||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
@ -44,6 +43,27 @@ ignore() ->
|
|||||||
gh_client_man:ignore().
|
gh_client_man:ignore().
|
||||||
|
|
||||||
|
|
||||||
|
-spec start() -> ok.
|
||||||
|
%% @doc
|
||||||
|
%% Start the server in an "ignore" state.
|
||||||
|
|
||||||
|
start() ->
|
||||||
|
ok = application:ensure_started(sasl),
|
||||||
|
ok = application:start(gex_httpd),
|
||||||
|
io:format("Starting...").
|
||||||
|
|
||||||
|
|
||||||
|
%-spec start(PortNum) -> ok
|
||||||
|
% when PortNum :: inet:port_number().
|
||||||
|
%%% @doc
|
||||||
|
%%% Start the server and begin listening immediately. Slightly more convenient when
|
||||||
|
%%% playing around in the shell.
|
||||||
|
%
|
||||||
|
%start(PortNum) ->
|
||||||
|
% ok = start(),
|
||||||
|
% ok = gh_client_man:listen(PortNum),
|
||||||
|
% io:format("Startup complete, listening on ~w~n", [PortNum]).
|
||||||
|
%
|
||||||
|
|
||||||
-spec start(normal, term()) -> {ok, pid()}.
|
-spec start(normal, term()) -> {ok, pid()}.
|
||||||
%% @private
|
%% @private
|
||||||
@ -53,25 +73,27 @@ ignore() ->
|
|||||||
|
|
||||||
start(normal, _Args) ->
|
start(normal, _Args) ->
|
||||||
Result = gh_sup:start_link(),
|
Result = gh_sup:start_link(),
|
||||||
ok = hz(),
|
|
||||||
% auto-listen to port 8000
|
% auto-listen to port 8000
|
||||||
|
ok = hz(),
|
||||||
ok = listen(8000),
|
ok = listen(8000),
|
||||||
Result.
|
Result.
|
||||||
|
|
||||||
|
|
||||||
hz() ->
|
hz() ->
|
||||||
ok = application:ensure_started(hakuzaru),
|
ok = application:ensure_started(hakuzaru),
|
||||||
%TestnetIP = {84, 46, 242, 9},
|
ok = hz:chain_nodes([testnet_node()]),
|
||||||
% fuck bulgaria
|
|
||||||
% TestnetIP = "groot.testnet.gajumaru.io",
|
|
||||||
% TestnetPort = 3013,
|
|
||||||
% japan good
|
|
||||||
TestnetIP = "tsuriai.jp",
|
|
||||||
TestnetPort = 4013,
|
|
||||||
TestnetNode = {TestnetIP, TestnetPort},
|
|
||||||
ok = hz:chain_nodes([TestnetNode]),
|
|
||||||
ok = tell("hz status: ~tp", [hz:status()]),
|
ok = tell("hz status: ~tp", [hz:status()]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
testnet_ip() ->
|
||||||
|
{84, 46, 242, 9}.
|
||||||
|
|
||||||
|
testnet_port() ->
|
||||||
|
3013.
|
||||||
|
|
||||||
|
testnet_node() ->
|
||||||
|
{testnet_ip(), testnet_port()}.
|
||||||
|
|
||||||
|
|
||||||
-spec stop(term()) -> ok.
|
-spec stop(term()) -> ok.
|
||||||
%% @private
|
%% @private
|
||||||
|
|||||||
@ -14,11 +14,9 @@
|
|||||||
%%% @end
|
%%% @end
|
||||||
|
|
||||||
-module(gh_client).
|
-module(gh_client).
|
||||||
|
|
||||||
-vsn("0.1.0").
|
-vsn("0.1.0").
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-copyright("2025-2026 QPQ AG").
|
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
|
|
||||||
-export([start/1]).
|
-export([start/1]).
|
||||||
@ -26,7 +24,6 @@
|
|||||||
-export([system_continue/3, system_terminate/4,
|
-export([system_continue/3, system_terminate/4,
|
||||||
system_get_state/1, system_replace_state/2]).
|
system_get_state/1, system_replace_state/2]).
|
||||||
|
|
||||||
-include("$zx_include/zx_logger.hrl").
|
|
||||||
-include("http.hrl").
|
-include("http.hrl").
|
||||||
|
|
||||||
|
|
||||||
@ -88,7 +85,7 @@ start_link(ListenSocket) ->
|
|||||||
%% call to listen/3.
|
%% call to listen/3.
|
||||||
|
|
||||||
init(Parent, ListenSocket) ->
|
init(Parent, ListenSocket) ->
|
||||||
ok = tell("~p Listening.~n", [self()]),
|
ok = io:format("~p Listening.~n", [self()]),
|
||||||
Debug = sys:debug_options([]),
|
Debug = sys:debug_options([]),
|
||||||
ok = proc_lib:init_ack(Parent, {ok, self()}),
|
ok = proc_lib:init_ack(Parent, {ok, self()}),
|
||||||
listen(Parent, Debug, ListenSocket).
|
listen(Parent, Debug, ListenSocket).
|
||||||
@ -112,12 +109,12 @@ listen(Parent, Debug, ListenSocket) ->
|
|||||||
{ok, Socket} ->
|
{ok, Socket} ->
|
||||||
{ok, _} = start(ListenSocket),
|
{ok, _} = start(ListenSocket),
|
||||||
{ok, Peer} = inet:peername(Socket),
|
{ok, Peer} = inet:peername(Socket),
|
||||||
ok = tell("~p Connection accepted from: ~p~n", [self(), Peer]),
|
ok = io:format("~p Connection accepted from: ~p~n", [self(), Peer]),
|
||||||
ok = gh_client_man:enroll(),
|
ok = gh_client_man:enroll(),
|
||||||
State = #s{socket = Socket},
|
State = #s{socket = Socket},
|
||||||
loop(Parent, Debug, State);
|
loop(Parent, Debug, State);
|
||||||
{error, closed} ->
|
{error, closed} ->
|
||||||
ok = log("~p Retiring: Listen socket closed.~n", [self()]),
|
ok = io:format("~p Retiring: Listen socket closed.~n", [self()]),
|
||||||
exit(normal)
|
exit(normal)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@ -134,6 +131,7 @@ loop(Parent, Debug, State = #s{socket = Socket, received = Received}) ->
|
|||||||
ok = inet:setopts(Socket, [{active, once}]),
|
ok = inet:setopts(Socket, [{active, once}]),
|
||||||
receive
|
receive
|
||||||
{tcp, Socket, Message} ->
|
{tcp, Socket, Message} ->
|
||||||
|
ok = io:format("~p received: ~tp~n", [self(), Message]),
|
||||||
%% Received exists because web browsers usually use the same
|
%% Received exists because web browsers usually use the same
|
||||||
%% acceptor socket for sequential requests
|
%% acceptor socket for sequential requests
|
||||||
%%
|
%%
|
||||||
@ -153,27 +151,21 @@ loop(Parent, Debug, State = #s{socket = Socket, received = Received}) ->
|
|||||||
%% see: https://git.qpq.swiss/QPQ-AG/QHL/pulls/1
|
%% see: https://git.qpq.swiss/QPQ-AG/QHL/pulls/1
|
||||||
case qhl:parse(Socket, Message2) of
|
case qhl:parse(Socket, Message2) of
|
||||||
{ok, Request, NewReceived} ->
|
{ok, Request, NewReceived} ->
|
||||||
try
|
ok = handle_request(Socket, Request),
|
||||||
ok = handle_request(Socket, Request)
|
|
||||||
catch
|
|
||||||
X:Y:Z ->
|
|
||||||
tell(error, "~tp ~tp: CRASH: ~tp:~tp:~tp, returning 500", [?MODULE, self(), X, Y, Z]),
|
|
||||||
http_err(Socket, 500)
|
|
||||||
end,
|
|
||||||
NewState = State#s{received = NewReceived},
|
NewState = State#s{received = NewReceived},
|
||||||
loop(Parent, Debug, NewState);
|
loop(Parent, Debug, NewState);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
tell(warning, "~p ~p: http parse error: ~tp~n", [?MODULE, self(), Reason]),
|
io:format("~p error: ~tp~n", [self(), Reason]),
|
||||||
ok = http_err(Socket, 500),
|
ok = http_err(Socket, 500),
|
||||||
exit(normal)
|
exit(normal)
|
||||||
end;
|
end;
|
||||||
{tcp_closed, Socket} ->
|
{tcp_closed, Socket} ->
|
||||||
ok = log(warning, "~p ~p: Socket closed, retiring.~n", [?MODULE, self()]),
|
ok = io:format("~p Socket closed, retiring.~n", [self()]),
|
||||||
exit(normal);
|
exit(normal);
|
||||||
{system, From, Request} ->
|
{system, From, Request} ->
|
||||||
sys:handle_system_msg(Request, From, Parent, ?MODULE, Debug, State);
|
sys:handle_system_msg(Request, From, Parent, ?MODULE, Debug, State);
|
||||||
Unexpected ->
|
Unexpected ->
|
||||||
ok = tell("~p ~p: Unexpected message: ~tp", [?MODULE, self(), Unexpected]),
|
ok = io:format("~p Unexpected message: ~tp", [self(), Unexpected]),
|
||||||
loop(Parent, Debug, State)
|
loop(Parent, Debug, State)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@ -238,54 +230,26 @@ system_replace_state(StateFun, State) ->
|
|||||||
|
|
||||||
%% ref: https://git.qpq.swiss/QPQ-AG/QHL/src/commit/7f77f9e3b19f58006df88a2a601e85835d300c37/include/http.hrl
|
%% ref: https://git.qpq.swiss/QPQ-AG/QHL/src/commit/7f77f9e3b19f58006df88a2a601e85835d300c37/include/http.hrl
|
||||||
|
|
||||||
handle_request(Socket, #request{method = get, path = Path}) ->
|
handle_request(Socket, #request{method = get, path = <<"/">>}) ->
|
||||||
% future-proofing for hardcoded paths
|
IndexHtmlPath = filename:join([zx:get_home(), "priv", "index.html"]),
|
||||||
case Path of
|
case file:read_file(IndexHtmlPath) of
|
||||||
_ -> handle_static(Socket, Path)
|
{ok, ResponseBody} ->
|
||||||
|
%% see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Messages#http_responses
|
||||||
|
Headers = [{"content-type", "text/html"}],
|
||||||
|
Response = #response{headers = Headers,
|
||||||
|
body = ResponseBody},
|
||||||
|
respond(Socket, Response);
|
||||||
|
Error ->
|
||||||
|
io:format("~p error: ~p~n", [self(), Error]),
|
||||||
|
http_err(Socket, 500)
|
||||||
end;
|
end;
|
||||||
handle_request(Socket, _) ->
|
handle_request(Socket, _) ->
|
||||||
http_err(Socket, 404).
|
http_err(Socket, 404).
|
||||||
|
|
||||||
|
|
||||||
-spec handle_static(Socket, Path) -> ok
|
|
||||||
when Socket :: gen_tcp:socket(),
|
|
||||||
Path :: binary().
|
|
||||||
|
|
||||||
handle_static(Socket, <<"/">>) ->
|
|
||||||
handle_static(Socket, <<"/index.html">>);
|
|
||||||
handle_static(Socket, Path) ->
|
|
||||||
case gh_sfc:query(Path) of
|
|
||||||
{found, Entry} -> handle_entry(Socket, Entry);
|
|
||||||
not_found -> http_err(Socket, 404)
|
|
||||||
end.
|
|
||||||
|
|
||||||
|
|
||||||
-spec handle_entry(Socket, Entry) -> ok
|
|
||||||
when Socket :: gen_tcp:socket(),
|
|
||||||
Entry :: gh_sfc_entry:entry().
|
|
||||||
|
|
||||||
handle_entry(Socket, Entry) ->
|
|
||||||
% -type encoding() :: none | gzip.
|
|
||||||
% -record(e, {fs_path :: file:filename(),
|
|
||||||
% last_modified :: file:date_time(),
|
|
||||||
% mime_type :: string(),
|
|
||||||
% encoding :: encoding(),
|
|
||||||
% contents :: binary()}).
|
|
||||||
Encoding = gh_sfc_entry:encoding(Entry) ,
|
|
||||||
MimeType = gh_sfc_entry:mime_type(Entry),
|
|
||||||
Contents = gh_sfc_entry:contents(Entry),
|
|
||||||
Headers0 =
|
|
||||||
case Encoding of
|
|
||||||
gzip -> [{"content-encoding", "gzip"}];
|
|
||||||
none -> []
|
|
||||||
end,
|
|
||||||
Headers1 = [{"content-type", MimeType} | Headers0],
|
|
||||||
Response = #response{headers = Headers1,
|
|
||||||
body = Contents},
|
|
||||||
respond(Socket, Response).
|
|
||||||
|
|
||||||
http_err(Socket, 404) ->
|
http_err(Socket, 404) ->
|
||||||
ResponseBody = bdy_404(),
|
HtmlPath = filename:join([zx:get_home(), "priv", "404.html"]),
|
||||||
|
{ok, ResponseBody} = file:read_file(HtmlPath),
|
||||||
Headers = [{"content-type", "text/html"}],
|
Headers = [{"content-type", "text/html"}],
|
||||||
Response = #response{headers = Headers,
|
Response = #response{headers = Headers,
|
||||||
code = 404,
|
code = 404,
|
||||||
@ -293,7 +257,8 @@ http_err(Socket, 404) ->
|
|||||||
respond(Socket, Response);
|
respond(Socket, Response);
|
||||||
% default error is 500
|
% default error is 500
|
||||||
http_err(Socket, _) ->
|
http_err(Socket, _) ->
|
||||||
ResponseBody = bdy_500(),
|
HtmlPath = filename:join([zx:get_home(), "priv", "500.html"]),
|
||||||
|
{ok, ResponseBody} = file:read_file(HtmlPath),
|
||||||
Headers = [{"content-type", "text/html"}],
|
Headers = [{"content-type", "text/html"}],
|
||||||
Response = #response{headers = Headers,
|
Response = #response{headers = Headers,
|
||||||
code = 500,
|
code = 500,
|
||||||
@ -301,39 +266,11 @@ http_err(Socket, _) ->
|
|||||||
respond(Socket, Response).
|
respond(Socket, Response).
|
||||||
|
|
||||||
|
|
||||||
bdy_404() ->
|
|
||||||
["<!DOCTYPE html>"
|
|
||||||
"<html lang=\"en\">"
|
|
||||||
"<head>"
|
|
||||||
"<meta charset=\"UTF-8\">"
|
|
||||||
"<title>QHL: 404</title>"
|
|
||||||
"</head>"
|
|
||||||
"<body>"
|
|
||||||
"<h1>404 Not Found</h1>"
|
|
||||||
"</body>"
|
|
||||||
"</html>"].
|
|
||||||
|
|
||||||
|
|
||||||
bdy_500() ->
|
|
||||||
["<!DOCTYPE html>"
|
|
||||||
"<html lang=\"en\">"
|
|
||||||
"<head>"
|
|
||||||
"<meta charset=\"UTF-8\">"
|
|
||||||
"<title>QHL: 500 Internal Server Error</title>"
|
|
||||||
"</head>"
|
|
||||||
"<body>"
|
|
||||||
"<h1>500 Internal Server Error</h1>"
|
|
||||||
"</body>"
|
|
||||||
"</html>"].
|
|
||||||
|
|
||||||
|
|
||||||
respond(Socket, R = #response{code = Code, headers = Headers, body = Body}) ->
|
respond(Socket, R = #response{code = Code, headers = Headers, body = Body}) ->
|
||||||
Slogan = slogan(Code),
|
Slogan = slogan(Code),
|
||||||
BodyBytes = iolist_to_binary(Body),
|
ContentLength = byte_size(Body),
|
||||||
ContentLength = byte_size(BodyBytes),
|
DefaultHeaders = [{"date", qhl:ridiculous_web_date()},
|
||||||
DefaultHeaders = [{"Server", "gex_httpd 0.1.0"},
|
{"content-length", integer_to_list(ContentLength)}],
|
||||||
{"Date", qhl:ridiculous_web_date()},
|
|
||||||
{"Content-Length", integer_to_list(ContentLength)}],
|
|
||||||
Headers2 = merge_headers(DefaultHeaders, Headers),
|
Headers2 = merge_headers(DefaultHeaders, Headers),
|
||||||
really_respond(Socket, R#response{slogan = Slogan,
|
really_respond(Socket, R#response{slogan = Slogan,
|
||||||
headers = Headers2}).
|
headers = Headers2}).
|
||||||
|
|||||||
@ -10,12 +10,10 @@
|
|||||||
%%% @end
|
%%% @end
|
||||||
|
|
||||||
-module(gh_client_man).
|
-module(gh_client_man).
|
||||||
-behavior(gen_server).
|
|
||||||
|
|
||||||
-vsn("0.1.0").
|
-vsn("0.1.0").
|
||||||
|
-behavior(gen_server).
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-copyright("2025-2026 QPQ AG").
|
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
|
|
||||||
-export([listen/1, ignore/0]).
|
-export([listen/1, ignore/0]).
|
||||||
@ -25,8 +23,6 @@
|
|||||||
code_change/3, terminate/2]).
|
code_change/3, terminate/2]).
|
||||||
|
|
||||||
|
|
||||||
-include("$zx_include/zx_logger.hrl").
|
|
||||||
|
|
||||||
%%% Type and Record Definitions
|
%%% Type and Record Definitions
|
||||||
|
|
||||||
|
|
||||||
@ -97,7 +93,7 @@ start_link() ->
|
|||||||
%% preparatory work necessary for proper function.
|
%% preparatory work necessary for proper function.
|
||||||
|
|
||||||
init(none) ->
|
init(none) ->
|
||||||
ok = tell("~p ~p: Starting.~n", [?MODULE, self()]),
|
ok = io:format("Starting.~n"),
|
||||||
State = #s{},
|
State = #s{},
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
|
|
||||||
@ -123,7 +119,7 @@ handle_call({listen, PortNum}, _, State) ->
|
|||||||
{Response, NewState} = do_listen(PortNum, State),
|
{Response, NewState} = do_listen(PortNum, State),
|
||||||
{reply, Response, NewState};
|
{reply, Response, NewState};
|
||||||
handle_call(Unexpected, From, State) ->
|
handle_call(Unexpected, From, State) ->
|
||||||
ok = tell("~p ~p Unexpected call from ~tp: ~tp~n", [?MODULE, self(), From, Unexpected]),
|
ok = io:format("~p Unexpected call from ~tp: ~tp~n", [self(), From, Unexpected]),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
|
||||||
@ -142,7 +138,7 @@ handle_cast(ignore, State) ->
|
|||||||
NewState = do_ignore(State),
|
NewState = do_ignore(State),
|
||||||
{noreply, NewState};
|
{noreply, NewState};
|
||||||
handle_cast(Unexpected, State) ->
|
handle_cast(Unexpected, State) ->
|
||||||
ok = tell("~p Unexpected cast: ~tp~n", [self(), Unexpected]),
|
ok = io:format("~p Unexpected cast: ~tp~n", [self(), Unexpected]),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
|
||||||
@ -158,7 +154,7 @@ handle_info({'DOWN', Mon, process, Pid, Reason}, State) ->
|
|||||||
NewState = handle_down(Mon, Pid, Reason, State),
|
NewState = handle_down(Mon, Pid, Reason, State),
|
||||||
{noreply, NewState};
|
{noreply, NewState};
|
||||||
handle_info(Unexpected, State) ->
|
handle_info(Unexpected, State) ->
|
||||||
ok = tell("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
|
||||||
@ -210,6 +206,7 @@ terminate(_, _) ->
|
|||||||
do_listen(PortNum, State = #s{port_num = none}) ->
|
do_listen(PortNum, State = #s{port_num = none}) ->
|
||||||
SocketOptions =
|
SocketOptions =
|
||||||
[inet6,
|
[inet6,
|
||||||
|
{packet, line},
|
||||||
{active, once},
|
{active, once},
|
||||||
{mode, binary},
|
{mode, binary},
|
||||||
{keepalive, true},
|
{keepalive, true},
|
||||||
@ -218,7 +215,7 @@ do_listen(PortNum, State = #s{port_num = none}) ->
|
|||||||
{ok, _} = gh_client:start(Listener),
|
{ok, _} = gh_client:start(Listener),
|
||||||
{ok, State#s{port_num = PortNum, listener = Listener}};
|
{ok, State#s{port_num = PortNum, listener = Listener}};
|
||||||
do_listen(_, State = #s{port_num = PortNum}) ->
|
do_listen(_, State = #s{port_num = PortNum}) ->
|
||||||
ok = tell("~p ~p: Already listening on ~p~n", [?MODULE, self(), PortNum]),
|
ok = io:format("~p Already listening on ~p~n", [self(), PortNum]),
|
||||||
{{error, {listening, PortNum}}, State}.
|
{{error, {listening, PortNum}}, State}.
|
||||||
|
|
||||||
|
|
||||||
@ -244,7 +241,7 @@ do_enroll(Pid, State = #s{clients = Clients}) ->
|
|||||||
case lists:member(Pid, Clients) of
|
case lists:member(Pid, Clients) of
|
||||||
false ->
|
false ->
|
||||||
Mon = monitor(process, Pid),
|
Mon = monitor(process, Pid),
|
||||||
ok = tell("Monitoring ~tp @ ~tp~n", [Pid, Mon]),
|
ok = io:format("Monitoring ~tp @ ~tp~n", [Pid, Mon]),
|
||||||
State#s{clients = [Pid | Clients]};
|
State#s{clients = [Pid | Clients]};
|
||||||
true ->
|
true ->
|
||||||
State
|
State
|
||||||
@ -271,6 +268,6 @@ handle_down(Mon, Pid, Reason, State = #s{clients = Clients}) ->
|
|||||||
State#s{clients = NewClients};
|
State#s{clients = NewClients};
|
||||||
false ->
|
false ->
|
||||||
Unexpected = {'DOWN', Mon, process, Pid, Reason},
|
Unexpected = {'DOWN', Mon, process, Pid, Reason},
|
||||||
ok = tell("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
||||||
State
|
State
|
||||||
end.
|
end.
|
||||||
|
|||||||
@ -14,12 +14,12 @@
|
|||||||
%%% @end
|
%%% @end
|
||||||
|
|
||||||
-module(gh_client_sup).
|
-module(gh_client_sup).
|
||||||
-behaviour(supervisor).
|
|
||||||
|
|
||||||
-vsn("0.1.0").
|
-vsn("0.1.0").
|
||||||
|
-behaviour(supervisor).
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-copyright("2025-2026 QPQ AG").
|
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
|
|
||||||
-export([start_acceptor/1]).
|
-export([start_acceptor/1]).
|
||||||
-export([start_link/0]).
|
-export([start_link/0]).
|
||||||
|
|||||||
@ -9,12 +9,10 @@
|
|||||||
%%% @end
|
%%% @end
|
||||||
|
|
||||||
-module(gh_clients).
|
-module(gh_clients).
|
||||||
-behavior(supervisor).
|
|
||||||
|
|
||||||
-vsn("0.1.0").
|
-vsn("0.1.0").
|
||||||
|
-behavior(supervisor).
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-copyright("2025-2026 QPQ AG").
|
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
|
|
||||||
-export([start_link/0]).
|
-export([start_link/0]).
|
||||||
|
|||||||
@ -3,10 +3,6 @@
|
|||||||
% mostly wrappers for ec_utils and hakuzaru
|
% mostly wrappers for ec_utils and hakuzaru
|
||||||
-module(gh_ct).
|
-module(gh_ct).
|
||||||
|
|
||||||
-vsn("0.1.0").
|
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
|
||||||
-copyright("2025-2026 QPQ AG").
|
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
-export_type([
|
-export_type([
|
||||||
keypair/0
|
keypair/0
|
||||||
|
|||||||
@ -1,154 +0,0 @@
|
|||||||
% @doc static file cache
|
|
||||||
%
|
|
||||||
% polls priv/static for sheeeit
|
|
||||||
%
|
|
||||||
% Adapted from FEWD: https://git.qpq.swiss/pharpend/fewd/src/commit/9adbf67ebde14c7c1d8de70ec9b241e6d4ee6f45/src/fd_httpd_sfc.erl
|
|
||||||
-module(gh_sfc).
|
|
||||||
-behavior(gen_server).
|
|
||||||
|
|
||||||
-vsn("0.1.0").
|
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
|
||||||
-copyright("2025-2026 QPQ AG").
|
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
-export_type([
|
|
||||||
entry/0,
|
|
||||||
maybe_entry/0
|
|
||||||
]).
|
|
||||||
|
|
||||||
%% caller context: actual api
|
|
||||||
-export([
|
|
||||||
base_path/0,
|
|
||||||
renew/0,
|
|
||||||
query/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
%% caller context: startup
|
|
||||||
-export([start_link/0 ]).
|
|
||||||
%% gen_server callbacks (process context)
|
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
|
||||||
|
|
||||||
-include("$zx_include/zx_logger.hrl").
|
|
||||||
|
|
||||||
|
|
||||||
-type cache() :: gh_sfc_cache:cache().
|
|
||||||
-type entry() :: gh_sfc_entry:entry().
|
|
||||||
-type maybe_entry() :: {found, entry()} | not_found.
|
|
||||||
|
|
||||||
|
|
||||||
-record(s, {base_path = base_path() :: file:filename(),
|
|
||||||
cache = gh_sfc_cache:new(base_path()) :: cache(),
|
|
||||||
auto_renew_ms = 1_000 :: pos_integer()}).
|
|
||||||
|
|
||||||
-type state() :: #s{}.
|
|
||||||
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------
|
|
||||||
%% API (ACTUAL API / CALLER CONTEXT)
|
|
||||||
%%------------------------------------------------------------------
|
|
||||||
|
|
||||||
-spec base_path() -> file:filename().
|
|
||||||
|
|
||||||
base_path() ->
|
|
||||||
filename:join([zx:get_home(), "priv", "static"]).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec renew() -> ok.
|
|
||||||
|
|
||||||
renew() ->
|
|
||||||
gen_server:cast(?MODULE, renew).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec query(HttpPath) -> MaybeEntry
|
|
||||||
when HttpPath :: binary(),
|
|
||||||
MaybeEntry :: maybe_entry().
|
|
||||||
|
|
||||||
query(Path) ->
|
|
||||||
gen_server:call(?MODULE, {query, Path}).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------
|
|
||||||
%% API (STARTUP / CALLER CONTEXT)
|
|
||||||
%%------------------------------------------------------------------
|
|
||||||
|
|
||||||
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
|
|
||||||
|
|
||||||
start_link() ->
|
|
||||||
gen_server:start_link({local, ?MODULE}, ?MODULE, none, []).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------
|
|
||||||
%% API (GEN_SERVER CALLBACKS / PROCESS CONTEXT)
|
|
||||||
%%------------------------------------------------------------------
|
|
||||||
|
|
||||||
-spec init(Args) -> {ok, InitState}
|
|
||||||
when Args :: none,
|
|
||||||
InitState :: state().
|
|
||||||
|
|
||||||
init(none) ->
|
|
||||||
ok = tell("starting gh_sfc"),
|
|
||||||
InitState = #s{},
|
|
||||||
#s{auto_renew_ms = AutoRenewInterval} = InitState,
|
|
||||||
erlang:send_after(AutoRenewInterval, self(), auto_renew),
|
|
||||||
{ok, InitState}.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec handle_call(Request, From, State) -> MaybeReply
|
|
||||||
when Request :: term(),
|
|
||||||
From :: {pid(), Tag :: term()},
|
|
||||||
State :: state(),
|
|
||||||
MaybeReply :: {reply, Reply, NewState}
|
|
||||||
| {noreply, NewState},
|
|
||||||
Reply :: term(),
|
|
||||||
NewState :: State.
|
|
||||||
|
|
||||||
handle_call({query, Path}, _, State = #s{cache = Cache}) ->
|
|
||||||
Reply = gh_sfc_cache:query(Path, Cache),
|
|
||||||
{reply, Reply, State};
|
|
||||||
handle_call(Unexpected, From, State) ->
|
|
||||||
ok = log(warning, "~p ~p: unexpected call from ~p: ~p", [?MODULE, self(), From, Unexpected]),
|
|
||||||
{noreply, State}.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec handle_cast(Request, State) -> {noreply, NewState}
|
|
||||||
when Request :: term(),
|
|
||||||
State :: state(),
|
|
||||||
NewState :: State.
|
|
||||||
|
|
||||||
handle_cast(renew, State) ->
|
|
||||||
NewState = i_renew(State),
|
|
||||||
{noreply, NewState};
|
|
||||||
handle_cast(Unexpected, State) ->
|
|
||||||
ok = log(warning, "~p ~p: unexpected cast: ~p", [?MODULE, self(), Unexpected]),
|
|
||||||
{noreply, State}.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec handle_info(Info, State) -> {noreply, NewState}
|
|
||||||
when Info :: term(),
|
|
||||||
State :: state(),
|
|
||||||
NewState :: State.
|
|
||||||
|
|
||||||
handle_info(auto_renew, State = #s{auto_renew_ms = MS}) ->
|
|
||||||
erlang:send_after(MS, self(), auto_renew),
|
|
||||||
NewState = i_renew(State),
|
|
||||||
{noreply, NewState};
|
|
||||||
handle_info(Unexpected, State) ->
|
|
||||||
ok = log(warning, "~p ~p: unexpected info: ~p", [?MODULE, self(), Unexpected]),
|
|
||||||
{noreply, State}.
|
|
||||||
|
|
||||||
|
|
||||||
%%-------------------------------------------------------------------
|
|
||||||
%% INTERNALS
|
|
||||||
%%-------------------------------------------------------------------
|
|
||||||
|
|
||||||
i_renew(State = #s{base_path = BasePath}) ->
|
|
||||||
NewCache = gh_sfc_cache:new(BasePath),
|
|
||||||
NewState = State#s{cache = NewCache},
|
|
||||||
NewState.
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
% @doc
|
|
||||||
% cache data management.
|
|
||||||
%
|
|
||||||
% Not pure code because logging and spam filesystem. But not a process
|
|
||||||
%
|
|
||||||
% Adapted from FEWD: https://git.qpq.swiss/pharpend/fewd/src/commit/9adbf67ebde14c7c1d8de70ec9b241e6d4ee6f45/src/fd_httpd_sfc_cache.erl
|
|
||||||
-module(gh_sfc_cache).
|
|
||||||
|
|
||||||
-vsn("0.1.0").
|
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
|
||||||
-copyright("2025-2026 QPQ AG").
|
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
-export_type([
|
|
||||||
cache/0
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([
|
|
||||||
query/2,
|
|
||||||
new/0, new/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
-include("$zx_include/zx_logger.hrl").
|
|
||||||
|
|
||||||
-type cache() :: #{HttpPath :: binary() := Entry :: gh_sfc_entry:entry()}.
|
|
||||||
|
|
||||||
|
|
||||||
-spec query(HttpPath, Cache) -> Result
|
|
||||||
when HttpPath :: binary(),
|
|
||||||
Cache :: cache(),
|
|
||||||
Result :: {found, Entry}
|
|
||||||
| not_found,
|
|
||||||
Entry :: gh_sfc_entry:entry().
|
|
||||||
|
|
||||||
query(HttpPath, Cache) ->
|
|
||||||
case maps:find(HttpPath, Cache) of
|
|
||||||
{ok, Entry} -> {found, Entry};
|
|
||||||
error -> not_found
|
|
||||||
end.
|
|
||||||
|
|
||||||
|
|
||||||
-spec new() -> cache().
|
|
||||||
new() -> #{}.
|
|
||||||
|
|
||||||
|
|
||||||
-spec new(BasePath) -> cache()
|
|
||||||
when BasePath :: file:filename().
|
|
||||||
% @doc
|
|
||||||
% if you give a file path it just takes the parent dir
|
|
||||||
%
|
|
||||||
% recursively crawls through file tree and picks
|
|
||||||
%
|
|
||||||
% IO errors will be logged but will result in cache misses
|
|
||||||
|
|
||||||
new(BasePath) ->
|
|
||||||
case filelib:is_file(BasePath) of
|
|
||||||
true -> new2(BasePath);
|
|
||||||
false ->
|
|
||||||
tell("~p:new(~p): no such file or directory, returning empty cache", [?MODULE, BasePath]),
|
|
||||||
#{}
|
|
||||||
end.
|
|
||||||
|
|
||||||
new2(BasePath) ->
|
|
||||||
BaseDir =
|
|
||||||
case filelib:is_dir(BasePath) of
|
|
||||||
true -> filename:absname(BasePath);
|
|
||||||
false -> filename:absname(filename:dirname(BasePath))
|
|
||||||
end,
|
|
||||||
BBaseDir = unicode:characters_to_binary(BaseDir),
|
|
||||||
HandlePath =
|
|
||||||
fun(AbsPath, AccCache) ->
|
|
||||||
BAbsPath = unicode:characters_to_binary(AbsPath),
|
|
||||||
HttpPath = remove_prefix(BBaseDir, BAbsPath),
|
|
||||||
NewCache =
|
|
||||||
case gh_sfc_entry:new(AbsPath) of
|
|
||||||
{found, Entry} -> maps:put(HttpPath, Entry, AccCache);
|
|
||||||
not_found -> AccCache
|
|
||||||
end,
|
|
||||||
NewCache
|
|
||||||
end,
|
|
||||||
filelib:fold_files(_dir = BaseDir,
|
|
||||||
_match = ".+",
|
|
||||||
_recursive = true,
|
|
||||||
_fun = HandlePath,
|
|
||||||
_init_acc = #{}).
|
|
||||||
|
|
||||||
remove_prefix(Prefix, From) ->
|
|
||||||
Size = byte_size(Prefix),
|
|
||||||
<<Prefix:Size/bytes, Rest/bytes>> = From,
|
|
||||||
Rest.
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
% @doc non-servery functions for static file caching
|
|
||||||
%
|
|
||||||
% library code. Not pure code because logging and spam filesystem. but not a
|
|
||||||
% process
|
|
||||||
%
|
|
||||||
% Adapted from FEWD: https://git.qpq.swiss/pharpend/fewd/src/commit/9adbf67ebde14c7c1d8de70ec9b241e6d4ee6f45/src/fd_httpd_sfc_entry.erl
|
|
||||||
-module(gh_sfc_entry).
|
|
||||||
|
|
||||||
-vsn("0.1.0").
|
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
|
||||||
-copyright("2025-2026 QPQ AG").
|
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
-export_type([
|
|
||||||
encoding/0,
|
|
||||||
entry/0
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([
|
|
||||||
%% constructor
|
|
||||||
new/1,
|
|
||||||
%% accessors
|
|
||||||
fs_path/1, last_modified/1, mime_type/1, encoding/1, contents/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
-include("$zx_include/zx_logger.hrl").
|
|
||||||
|
|
||||||
%% types
|
|
||||||
|
|
||||||
% id = not compressed
|
|
||||||
-type encoding() :: none | gzip.
|
|
||||||
|
|
||||||
-record(e, {fs_path :: file:filename(),
|
|
||||||
last_modified :: file:date_time(),
|
|
||||||
mime_type :: string(),
|
|
||||||
encoding :: encoding(),
|
|
||||||
contents :: binary()}).
|
|
||||||
|
|
||||||
-opaque entry() :: #e{}.
|
|
||||||
|
|
||||||
%% accessors
|
|
||||||
|
|
||||||
fs_path(#e{fs_path = X}) -> X.
|
|
||||||
last_modified(#e{last_modified = X}) -> X.
|
|
||||||
mime_type(#e{mime_type = X}) -> X.
|
|
||||||
encoding(#e{encoding = X}) -> X.
|
|
||||||
contents(#e{contents = X}) -> X.
|
|
||||||
|
|
||||||
%% API
|
|
||||||
|
|
||||||
-spec new(Path) -> Result
|
|
||||||
when Path :: file:filename(),
|
|
||||||
Result :: {found, entry()}
|
|
||||||
| not_found.
|
|
||||||
% @doc
|
|
||||||
% absolute file path stored in resulting record
|
|
||||||
%
|
|
||||||
% returns not_found if ANY I/O error occurs during the process. will be logged
|
|
||||||
|
|
||||||
new(Path) ->
|
|
||||||
log(info, "~tp:new(~tp)", [?MODULE, Path]),
|
|
||||||
case file:read_file(Path) of
|
|
||||||
{ok, Binary} ->
|
|
||||||
{found, new2(Path, Binary)};
|
|
||||||
Error ->
|
|
||||||
tell("~tp:new(~tp): file read error: ~tp", [?MODULE, Path, Error]),
|
|
||||||
not_found
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% can assume file exists
|
|
||||||
new2(FsPath, FileBytes) ->
|
|
||||||
LastModified = filelib:last_modified(FsPath),
|
|
||||||
{Encoding, MimeType} = mimetype_compress(FsPath),
|
|
||||||
Contents =
|
|
||||||
case Encoding of
|
|
||||||
none -> FileBytes;
|
|
||||||
gzip -> zlib:gzip(FileBytes)
|
|
||||||
end,
|
|
||||||
#e{fs_path = FsPath,
|
|
||||||
last_modified = LastModified,
|
|
||||||
mime_type = MimeType,
|
|
||||||
encoding = Encoding,
|
|
||||||
contents = Contents}.
|
|
||||||
|
|
||||||
mimetype_compress(FsPath) ->
|
|
||||||
case string:casefold(filename:extension(FsPath)) of
|
|
||||||
%% only including the ones i anticipate encountering
|
|
||||||
%% plaintext formats
|
|
||||||
".css" -> {gzip, "text/css"};
|
|
||||||
".htm" -> {gzip, "text/html"};
|
|
||||||
".html" -> {gzip, "text/html"};
|
|
||||||
".js" -> {gzip, "text/javascript"};
|
|
||||||
".json" -> {gzip, "application/json"};
|
|
||||||
".map" -> {gzip, "application/json"};
|
|
||||||
".md" -> {gzip, "text/markdown"};
|
|
||||||
".ts" -> {gzip, "text/x-typescript"};
|
|
||||||
".txt" -> {gzip, "text/plain"};
|
|
||||||
%% binary formats
|
|
||||||
".gif" -> {none, "image/gif"};
|
|
||||||
".jpg" -> {none, "image/jpeg"};
|
|
||||||
".jpeg" -> {none, "image/jpeg"};
|
|
||||||
".mp4" -> {none, "video/mp4"};
|
|
||||||
".png" -> {none, "image/png"};
|
|
||||||
".webm" -> {none, "video/webm"};
|
|
||||||
".webp" -> {none, "image/webp"};
|
|
||||||
_ -> {none, "application/octet-stream"}
|
|
||||||
end.
|
|
||||||
@ -12,12 +12,11 @@
|
|||||||
%%% @end
|
%%% @end
|
||||||
|
|
||||||
-module(gh_sup).
|
-module(gh_sup).
|
||||||
-behaviour(supervisor).
|
|
||||||
|
|
||||||
-vsn("0.1.0").
|
-vsn("0.1.0").
|
||||||
|
-behaviour(supervisor).
|
||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-copyright("2025-2026 QPQ AG").
|
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-license("MIT").
|
|
||||||
|
|
||||||
-export([start_link/0]).
|
-export([start_link/0]).
|
||||||
-export([init/1]).
|
-export([init/1]).
|
||||||
@ -37,17 +36,11 @@ start_link() ->
|
|||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
RestartStrategy = {one_for_one, 1, 60},
|
RestartStrategy = {one_for_one, 1, 60},
|
||||||
StaticFileCache = {gh_sfc,
|
|
||||||
{gh_sfc, start_link, []},
|
|
||||||
permanent,
|
|
||||||
5000,
|
|
||||||
worker,
|
|
||||||
[gh_sfc]},
|
|
||||||
Clients = {gh_clients,
|
Clients = {gh_clients,
|
||||||
{gh_clients, start_link, []},
|
{gh_clients, start_link, []},
|
||||||
permanent,
|
permanent,
|
||||||
5000,
|
5000,
|
||||||
supervisor,
|
supervisor,
|
||||||
[gh_clients]},
|
[gh_clients]},
|
||||||
Children = [StaticFileCache, Clients],
|
Children = [Clients],
|
||||||
{ok, {RestartStrategy, Children}}.
|
{ok, {RestartStrategy, Children}}.
|
||||||
|
|||||||
@ -20,9 +20,9 @@
|
|||||||
{key_name,none}.
|
{key_name,none}.
|
||||||
{a_email,"peterharpending@qpq.swiss"}.
|
{a_email,"peterharpending@qpq.swiss"}.
|
||||||
{c_email,"peterharpending@qpq.swiss"}.
|
{c_email,"peterharpending@qpq.swiss"}.
|
||||||
{copyright,"2025-2026, QPQ AG"}.
|
{copyright,"Peter Harpending"}.
|
||||||
{file_exts,[]}.
|
{file_exts,[]}.
|
||||||
{license,mit}.
|
{license,skip}.
|
||||||
{repo_url,"https://git.qpq.swiss/QPQ-AG/gex"}.
|
{repo_url,"https://git.qpq.swiss/QPQ-AG/gex"}.
|
||||||
{tags,[]}.
|
{tags,[]}.
|
||||||
{ws_url,"https://git.qpq.swiss/QPQ-AG/gex"}.
|
{ws_url,"https://git.qpq.swiss/QPQ-AG/gex"}.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user