Browse Source

web skeleton

Maxim Sokhatsky 10 years ago
parent
commit
66d511f163

BIN
mad


+ 20 - 0
priv/web/apps/n2o_sample/priv/static/spa/index.htm

@@ -0,0 +1,20 @@
+<html>
+<head>
+<link href="/static/synrc.css?1" type="text/css" rel="stylesheet">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>N2O</title>
+</head>
+<body>
+<div id="history"></div>
+<input id="message" type="text">
+<button id="send" type="button">Chat</button>
+<script>var transition = {pid: '', port:'8000'}</script>
+<script src='/n2o/bullet.js?1' type='text/javascript' charset='utf-8'></script>
+<script src='/n2o/n2o.js?1' type='text/javascript' charset='utf-8'></script>
+<script src='/n2o/bert.js?1' type='text/javascript' charset='utf-8'></script>
+</script><div id="n2ostatus"></div>
+</body>
+</html>
+
+
+

+ 1 - 0
priv/web/apps/n2o_sample/priv/static/synrc.css

@@ -0,0 +1 @@
+button { font-size:20pt; padding: 10px; }

+ 15 - 0
priv/web/apps/n2o_sample/priv/templates/index.html

@@ -0,0 +1,15 @@
+<html>
+<head>
+<link href="/static/synrc.css?1" type="text/css" rel="stylesheet">
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>{{title}}</title>
+</head>
+<body>
+{{body}}
+<script>{{script}}</script>
+<script src='/n2o/bert.js?1' type='text/javascript' charset='utf-8'></script>
+<script src='/n2o/bullet.js?1' type='text/javascript' charset='utf-8'></script>
+<script src='/n2o/n2o.js?1' type='text/javascript' charset='utf-8'></script>
+<div id="n2ostatus"></div>
+</body>
+</html>

+ 7 - 0
priv/web/apps/n2o_sample/rebar.config

@@ -0,0 +1,7 @@
+{erlydtl_opts, [
+    {doc_root,   "priv/templates"},
+    {out_dir,    "ebin"},
+    {compiler_options, [report, return, debug_info]},
+    {source_ext, ".html"},
+    {module_ext, "_view"}
+]}.

+ 20 - 0
priv/web/apps/n2o_sample/src/index.erl

@@ -0,0 +1,20 @@
+-module(index).
+-compile(export_all).
+-include_lib("n2o/include/wf.hrl").
+
+peer()    -> io_lib:format("~p",[wf:peer(?REQ)]).
+message() -> wf:js_escape(wf:html_encode(wf:q(message))).
+main()    -> #dtl{file="index",app=n2o_sample,bindings=[{body,body()}]}.
+body() ->
+    {ok,Pid} = wf:comet(fun() -> chat_loop() end), 
+    [ #panel{id=history}, #textbox{id=message},
+      #button{id=send,body="Chat",postback={chat,Pid},source=[message]} ].
+
+event(init) -> wf:reg(room);
+event({chat,Pid}) -> Pid ! {peer(), message()};
+event(Event) -> skip.
+
+chat_loop() ->
+    receive {Peer, Message} -> 
+       wf:insert_bottom(history,#panel{id=history,body=[Peer,": ",Message,#br{}]}),
+       wf:flush(room) end, chat_loop().

+ 204 - 0
priv/web/apps/n2o_sample/src/n2o_dynalo.erl

@@ -0,0 +1,204 @@
+-module(n2o_dynalo).
+-compile(export_all).
+
+-export([init/3]).
+-export([rest_init/2]).
+-export([malformed_request/2]).
+-export([forbidden/2]).
+-export([content_types_provided/2]).
+-export([resource_exists/2]).
+-export([last_modified/2]).
+-export([generate_etag/2]).
+-export([get_file/2]).
+
+-type extra_etag() :: {etag, module(), function()} | {etag, false}.
+-type extra_mimetypes() :: {mimetypes, module(), function()}
+	| {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
+-type extra() :: [extra_etag() | extra_mimetypes()].
+-type opts() :: {file | dir, string() | binary()}
+	| {file | dir, string() | binary(), extra()}
+	| {priv_file | priv_dir, atom(), string() | binary()}
+	| {priv_file | priv_dir, atom(), string() | binary(), extra()}.
+-export_type([opts/0]).
+
+-include_lib("kernel/include/file.hrl").
+
+-type state() :: {binary(), {ok, #file_info{}} | {error, atom()}, extra()}.
+
+init(_, _, _) ->
+	{upgrade, protocol, cowboy_rest}.
+
+%% @doc Resolve the file that will be sent and get its file information.
+%% If the handler is configured to manage a directory, check that the
+%% requested file is inside the configured directory.
+
+-spec rest_init(Req, opts())
+	-> {ok, Req, error | state()}
+	when Req::cowboy_req:req().
+rest_init(Req, {Name, Path}) ->
+	rest_init_opts(Req, {Name, Path, []});
+rest_init(Req, {Name, App, Path})
+		when Name =:= priv_file; Name =:= priv_dir ->
+	rest_init_opts(Req, {Name, App, Path, []});
+rest_init(Req, Opts) ->
+	rest_init_opts(Req, Opts).
+
+rest_init_opts(Req, {priv_file, App, Path, Extra}) ->
+	rest_init_info(Req, absname(priv_path(App, Path)), Extra);
+rest_init_opts(Req, {file, Path, Extra}) ->
+	rest_init_info(Req, absname(Path), Extra);
+rest_init_opts(Req, {priv_dir, App, Path, Extra}) ->
+	rest_init_dir(Req, priv_path(App, Path), Extra);
+rest_init_opts(Req, {dir, Path, Extra}) ->
+	rest_init_dir(Req, Path, Extra).
+
+priv_path(App=n2o_sample, Path) -> priv_path(App, Path, "apps/");
+priv_path(App, Path) -> priv_path(App, Path, "deps/").
+
+priv_path(App, Path, Prefix) ->
+    LApp = Prefix ++ atom_to_list(App) ++ "/priv",
+	case LApp of
+		PrivDir when is_list(Path) ->
+			PrivDir ++ "/" ++ Path;
+		PrivDir when is_binary(Path) ->
+			<< (list_to_binary(PrivDir))/binary, $/, Path/binary >>
+	end.
+
+absname(Path) when is_list(Path) ->
+	filename:absname(list_to_binary(Path));
+absname(Path) when is_binary(Path) ->
+	filename:absname(Path).
+
+rest_init_dir(Req, Path, Extra) when is_list(Path) ->
+	rest_init_dir(Req, list_to_binary(Path), Extra);
+rest_init_dir(Req, Path, Extra) ->
+	Dir = fullpath(Path), %filename:absname(Path)),
+	{PathInfo, Req2} = cowboy_req:path_info(Req),
+	Filepath = filename:join([Dir|PathInfo]),
+	Len = byte_size(Dir),
+	case fullpath(Filepath) of
+		<< Dir:Len/binary, $/, _/binary >> ->
+			rest_init_info(Req2, Filepath, Extra);
+		_ ->
+			{ok, Req2, error}
+	end.
+
+fullpath(Path) ->
+	fullpath(filename:split(Path), []).
+fullpath([], Acc) ->
+	filename:join(lists:reverse(Acc));
+fullpath([<<".">>|Tail], Acc) ->
+	fullpath(Tail, Acc);
+fullpath([<<"..">>|Tail], Acc=[_]) ->
+	fullpath(Tail, Acc);
+fullpath([<<"..">>|Tail], [_|Acc]) ->
+	fullpath(Tail, Acc);
+fullpath([Segment|Tail], Acc) ->
+	fullpath(Tail, [Segment|Acc]).
+
+rest_init_info(Req, Path, Extra) ->
+%    io:format("File Requested: ~p ~n\r",[Path]),
+	Info = {ok, #file_info{type=regular,size=0}},
+	 %file:read_file_info(Path, [{time, universal}]),
+	{ok, Req, {Path, Info, Extra}}.
+
+
+%% @doc Reject requests that tried to access a file outside
+%% the target directory.
+
+-spec malformed_request(Req, State)
+	-> {boolean(), Req, State}.
+malformed_request(Req, State) ->
+	{State =:= error, Req, State}.
+
+%% @doc Directories, files that can't be accessed at all and
+%% files with no read flag are forbidden.
+
+-spec forbidden(Req, State)
+	-> {boolean(), Req, State}
+	when State::state().
+forbidden(Req, State={_, {ok, #file_info{type=directory}}, _}) ->
+	{true, Req, State};
+forbidden(Req, State={_, {error, eacces}, _}) ->
+	{true, Req, State};
+forbidden(Req, State={_, {ok, #file_info{access=Access}}, _})
+		when Access =:= write; Access =:= none ->
+	{true, Req, State};
+forbidden(Req, State) ->
+	{false, Req, State}.
+
+%% @doc Detect the mimetype of the file.
+
+-spec content_types_provided(Req, State)
+	-> {[{binary(), get_file}], Req, State}
+	when State::state().
+content_types_provided(Req, State={Path, _, Extra}) ->
+	case lists:keyfind(mimetypes, 1, Extra) of
+		false ->
+			{[{cow_mimetypes:web(Path), get_file}], Req, State};
+		{mimetypes, Module, Function} ->
+			{[{Module:Function(Path), get_file}], Req, State};
+		{mimetypes, Type} ->
+			{[{Type, get_file}], Req, State}
+	end.
+
+%% @doc Assume the resource doesn't exist if it's not a regular file.
+
+-spec resource_exists(Req, State)
+	-> {boolean(), Req, State}
+	when State::state().
+resource_exists(Req, State={_, {ok, #file_info{type=regular}}, _}) ->
+	{true, Req, State};
+resource_exists(Req, State) ->
+	{false, Req, State}.
+
+%% @doc Generate an etag for the file.
+
+-spec generate_etag(Req, State)
+	-> {{strong | weak, binary()}, Req, State}
+	when State::state().
+generate_etag(Req, State={Path, {ok, #file_info{size=Size, mtime=Mtime}},
+		Extra}) ->
+	case lists:keyfind(etag, 1, Extra) of
+		false ->
+			{generate_default_etag(Size, Mtime), Req, State};
+		{etag, Module, Function} ->
+			{Module:Function(Path, Size, Mtime), Req, State};
+		{etag, false} ->
+			{undefined, Req, State}
+	end.
+
+generate_default_etag(Size, Mtime) ->
+	{strong, list_to_binary(integer_to_list(
+		erlang:phash2({Size, Mtime}, 16#ffffffff)))}.
+
+%% @doc Return the time of last modification of the file.
+
+-spec last_modified(Req, State)
+	-> {calendar:datetime(), Req, State}
+	when State::state().
+last_modified(Req, State={_, {ok, #file_info{mtime=Modified}}, _}) ->
+	{Modified, Req, State}.
+
+%% @doc Stream the file.
+%% @todo Export cowboy_req:resp_body_fun()?
+
+-spec get_file(Req, State)
+	-> {{stream, non_neg_integer(), fun()}, Req, State}
+	when State::state().
+get_file(Req, State={Path, {ok, #file_info{size=Size}}, _}) ->
+    StringPath = binary_to_list(Path),
+	FileName = absname(StringPath),
+    Raw = case file:read_file(FileName) of
+         {ok,Bin} -> Bin;
+         {error,_} -> mad_repl:load_file(StringPath) end,
+%    io:format("Cowboy Requested Static File: ~p~n\r ~p~n\r",[Raw,absname(StringPath)]),
+	Sendfile = fun (Socket, Transport) ->
+		case Transport:send(Socket, Raw) of
+			{ok, _} -> ok;
+			{error, closed} -> ok;
+			{error, etimedout} -> ok;
+			_ -> ok
+		end
+	end,
+	{{stream, size(Raw), Sendfile}, Req, State}.

+ 9 - 0
priv/web/apps/n2o_sample/src/n2o_sample.app.src

@@ -0,0 +1,9 @@
+{application, n2o_sample,
+ [
+  {description, "N2O VXZ Sample App"},
+  {vsn, "1"},
+  {registered, []},
+  {applications, [kernel, stdlib, n2o]},
+  {mod, { web_app, []}},
+  {env, []}
+ ]}.

+ 22 - 0
priv/web/apps/n2o_sample/src/routes.erl

@@ -0,0 +1,22 @@
+-module(routes).
+-author('Maxim Sokhatsky').
+-include_lib("n2o/include/wf.hrl").
+-export([init/2, finish/2]).
+
+finish(State, Ctx) -> {ok, State, Ctx}.
+init(State, Ctx) -> 
+    Path = wf:path(Ctx#context.req),
+    wf:info(?MODULE,"Route: ~p~n",[Path]),
+    {ok, State, Ctx#context{path=Path,module=route_prefix(Path)}}.
+
+route_prefix(<<"/ws/",P/binary>>) -> route(P);
+route_prefix(<<"/",P/binary>>) -> route(P);
+route_prefix(P) -> route(P).
+
+route(<<>>)              -> index;
+route(<<"index">>)       -> index;
+route(<<"login">>)       -> login;
+route(<<"favicon.ico">>) -> static_file;
+route(<<"static/spa/spa.htm">>) -> login;
+route(<<"static/spa/index.htm">>) -> index;
+route(_) -> index.

+ 8 - 0
priv/web/apps/n2o_sample/src/web_app.erl

@@ -0,0 +1,8 @@
+-module(web_app).
+-behaviour(application).
+-export([start/0, start/2, stop/1, main/1]).
+
+main(A) -> mad_repl:main(A).
+start() -> start(normal, []).
+start(_StartType, _StartArgs) -> web_sup:start_link().
+stop(_State) -> ok.

+ 19 - 0
priv/web/apps/n2o_sample/src/web_sup.erl

@@ -0,0 +1,19 @@
+-module(web_sup).
+-behaviour(supervisor).
+-export([start_link/0, init/1]).
+
+start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+mime() -> [{mimetypes,cow_mimetypes,all}].
+rules() -> cowboy_router:compile(
+    [{'_', [
+        {"/static/[...]", n2o_dynalo, {dir, "apps/n2o_sample/priv/static", mime()}},
+        {"/n2o/[...]", n2o_dynalo, {dir, "deps/n2o/priv", mime()}},
+        {"/rest/:resource", rest_cowboy, []},
+        {"/rest/:resource/:id", rest_cowboy, []},
+        {"/ws/[...]", bullet_handler, [{handler, n2o_bullet}]},
+        {'_', n2o_cowboy, []}
+    ]}]).
+
+init([]) ->
+    cowboy:start_http(http, 3, [{port, wf:config(n2o,port)}], [{env, [{dispatch, rules()}]}]),
+    {ok, {{one_for_one, 5, 10}, []}}.

+ 3 - 0
priv/web/apps/rebar.config

@@ -0,0 +1,3 @@
+{sub_dirs, [ "n2o_sample" ]}.
+{lib_dirs, ["../apps"]}.
+{deps_dir, ["../deps"]}.

BIN
priv/web/mad


+ 13 - 0
priv/web/rebar.config

@@ -0,0 +1,13 @@
+{sub_dirs,["apps"]}.
+{deps_dir,"deps"}.
+{deps, [
+    {erlydtl,".*", {git, "git://github.com/evanmiller/erlydtl", {tag, "0.8.0"} }},
+    {cowboy, ".*", {git, "git://github.com/extend/cowboy",      {tag, "0.9.0"} }},
+    {gproc,  ".*", {git, "git://github.com/uwiger/gproc.git",   {tag, "0.3"}   }},
+    {mad,    ".*", {git, "git://github.com/synrc/mad",          {tag, "master"}   }},
+    {fs,     ".*", {git, "git://github.com/synrc/fs",           {tag, "0.8"}   }},
+    {sh,     ".*", {git, "git://github.com/synrc/sh",           {tag, "0.8"}   }},
+    {active, ".*", {git, "git://github.com/synrc/active",       {tag, "master"}   }},
+    {n2o,    ".*", {git, "git://github.com/5HT/n2o",            {tag, "master"} }}
+]}.
+

+ 4 - 0
priv/web/sys.config

@@ -0,0 +1,4 @@
+[
+ {n2o, [{port,8000},{route,routes}]},
+ {kvs, [{dba,store_mnesia}, {schema, [kvs_user, kvs_acl, kvs_feed, kvs_subscription ]} ]}
+].