-module(dream@controllers@static). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/dream/controllers/static.gleam"). -export([default_config/0, with_directory_listing/1, with_custom_404/2, without_index/1, serve/6]). -export_type([config/2]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Secure static file serving\n" "\n" " Serve files from disk with built-in security. Handles CSS, JavaScript, images,\n" " and any other static assets your application needs.\n" "\n" " ## Quick Setup\n" "\n" " ```gleam\n" " import dream/controllers/static.{default_config, serve}\n" " import dream/http/request.{type Request, Get, get_string_param}\n" " import dream/http/response.{type Response, json_response}\n" " import dream/http/status.{bad_request}\n" " import dream/router.{route}\n" "\n" " pub fn serve_assets(request: Request, context, services) -> Response {\n" " case get_string_param(request, \"path\") {\n" " Ok(path) -> \n" " serve(\n" " request: request,\n" " context: context,\n" " services: services,\n" " root: \"./public\",\n" " filepath: path,\n" " config: default_config(),\n" " )\n" " Error(msg) -> json_response(bad_request, error_json(msg))\n" " }\n" " }\n" "\n" " // In your router:\n" " route(method: Get, path: \"/assets/**path\", controller: serve_assets, middleware: [])\n" " ```\n" "\n" " ## Security\n" "\n" " Built-in protection against:\n" " - Path traversal attacks (`../../../etc/passwd`)\n" " - Absolute path access (`/etc/passwd`)\n" " - Directory escaping\n" "\n" " Files outside the root directory return 404, never errors that reveal filesystem structure.\n" "\n" " ## Features\n" "\n" " - **MIME type detection** - Automatic content-type headers\n" " - **Index serving** - Serves `index.html` for directory requests\n" " - **Directory listing** - Optional file browser (disabled by default)\n" " - **Custom 404s** - Use your own not-found handler\n" " Control how directories and missing files are handled.\n" " - Serves `index.html` for directories\n" " - No directory listing\n" " - Standard 404 response for missing files\n" " Shows a file browser when a directory has no `index.html`. Use this for\n" " development or when you want users to browse files. Don't enable in production\n" " unless you specifically want directory browsing.\n" ). -type config(AFGL, AFGM) :: {config, boolean(), boolean(), gleam@option:option(fun((dream@http@request:request(), AFGL, AFGM) -> dream@http@response:response()))}. -file("src/dream/controllers/static.gleam", 82). ?DOC(" Default configuration with secure settings\n"). -spec default_config() -> config(any(), any()). default_config() -> {config, true, false, none}. -file("src/dream/controllers/static.gleam", 92). ?DOC(" Enable directory listing\n"). -spec with_directory_listing(config(AFGR, AFGS)) -> config(AFGR, AFGS). with_directory_listing(Config) -> {config, erlang:element(2, Config), true, erlang:element(4, Config)}. -file("src/dream/controllers/static.gleam", 99). ?DOC(" Set custom 404 handler\n"). -spec with_custom_404( config(AFGX, AFGY), fun((dream@http@request:request(), AFGX, AFGY) -> dream@http@response:response()) ) -> config(AFGX, AFGY). with_custom_404(Config, Handler) -> {config, erlang:element(2, Config), erlang:element(3, Config), {some, Handler}}. -file("src/dream/controllers/static.gleam", 107). ?DOC(" Disable automatic index.html serving\n"). -spec without_index(config(AFHD, AFHE)) -> config(AFHD, AFHE). without_index(Config) -> {config, false, erlang:element(3, Config), erlang:element(4, Config)}. -file("src/dream/controllers/static.gleam", 269). -spec build_html_response(binary()) -> dream@http@response:response(). build_html_response(Html) -> {response, 200, {text, Html}, [{header, <<"Content-Type"/utf8>>, <<"text/html; charset=utf-8"/utf8>>}], [], {some, <<"text/html; charset=utf-8"/utf8>>}}. -file("src/dream/controllers/static.gleam", 279). -spec build_directory_html(binary(), list(binary())) -> binary(). build_directory_html(Path, Entries) -> Title = <<"Index of "/utf8, Path/binary>>, Header = <<<<<<<<" "/utf8, Title/binary>>/binary, "

"/utf8>>/binary, Title/binary>>/binary, "

"/utf8>>, <<<<<
>/binary, Items/binary>>/binary, Footer/binary>>. -file("src/dream/controllers/static.gleam", 328). -spec default_404() -> dream@http@response:response(). default_404() -> {response, 404, {text, <<"

404 Not Found

"/utf8>>}, [{header, <<"Content-Type"/utf8>>, <<"text/html; charset=utf-8"/utf8>>}], [], {some, <<"text/html; charset=utf-8"/utf8>>}}. -file("src/dream/controllers/static.gleam", 255). -spec generate_directory_listing(binary(), binary()) -> dream@http@response:response(). generate_directory_listing(Directory, Request_path) -> case simplifile_erl:read_directory(Directory) of {ok, Entries} -> Sorted = gleam@list:sort(Entries, fun gleam@string:compare/2), Html = build_directory_html(Request_path, Sorted), build_html_response(Html); {error, _} -> default_404() end. -file("src/dream/controllers/static.gleam", 316). -spec handle_not_found( dream@http@request:request(), AFIG, AFIH, config(AFIG, AFIH) ) -> dream@http@response:response(). handle_not_found(Request, Context, Services, Config) -> case erlang:element(4, Config) of {some, Handler} -> Handler(Request, Context, Services); none -> default_404() end. -file("src/dream/controllers/static.gleam", 210). -spec handle_no_index( dream@http@request:request(), AFHZ, AFIA, binary(), config(AFHZ, AFIA) ) -> dream@http@response:response(). handle_no_index(Request, Context, Services, Directory, Config) -> case erlang:element(3, Config) of true -> generate_directory_listing(Directory, erlang:element(5, Request)); false -> handle_not_found(Request, Context, Services, Config) end. -file("src/dream/controllers/static.gleam", 339). ?DOC(" Validate path doesn't escape root directory\n"). -spec is_safe_path(binary()) -> boolean(). is_safe_path(Filepath) -> not gleam_stdlib:contains_string(Filepath, <<".."/utf8>>) andalso not gleam_stdlib:string_starts_with( Filepath, <<"/"/utf8>> ). -file("src/dream/controllers/static.gleam", 347). ?DOC(" Detect MIME type from file extension using marceau library\n"). -spec mime_type(binary()) -> binary(). mime_type(Filepath) -> case begin _pipe = gleam@string:split(Filepath, <<"."/utf8>>), gleam@list:last(_pipe) end of {ok, Ext} -> marceau:extension_to_mime_type(Ext); {error, _} -> <<"application/octet-stream"/utf8>> end. -file("src/dream/controllers/static.gleam", 239). -spec build_file_response(binary(), binary()) -> dream@http@response:response(). build_file_response(Content, Filepath) -> Mime = mime_type(Filepath), Size = erlang:byte_size(Content), {response, 200, {text, Content}, [{header, <<"Content-Type"/utf8>>, Mime}, {header, <<"Content-Length"/utf8>>, erlang:integer_to_binary(Size)}], [], {some, Mime}}. -file("src/dream/controllers/static.gleam", 231). -spec serve_file(binary()) -> dream@http@response:response(). serve_file(Filepath) -> case simplifile:read(Filepath) of {ok, Content} -> build_file_response(Content, Filepath); {error, _} -> default_404() end. -file("src/dream/controllers/static.gleam", 223). -spec try_serve_index(binary()) -> {ok, dream@http@response:response()} | {error, nil}. try_serve_index(Directory) -> Index_path = <>, case simplifile_erl:is_file(Index_path) of {ok, true} -> {ok, serve_file(Index_path)}; _ -> {error, nil} end. -file("src/dream/controllers/static.gleam", 191). -spec handle_directory( dream@http@request:request(), AFHV, AFHW, binary(), config(AFHV, AFHW) ) -> dream@http@response:response(). handle_directory(Request, Context, Services, Directory, Config) -> case erlang:element(2, Config) of true -> case try_serve_index(Directory) of {ok, Response} -> Response; {error, _} -> handle_no_index( Request, Context, Services, Directory, Config ) end; false -> handle_no_index(Request, Context, Services, Directory, Config) end. -file("src/dream/controllers/static.gleam", 178). -spec handle_directory_or_missing( dream@http@request:request(), AFHR, AFHS, binary(), config(AFHR, AFHS) ) -> dream@http@response:response(). handle_directory_or_missing(Request, Context, Services, Path, Config) -> case simplifile_erl:is_directory(Path) of {ok, true} -> handle_directory(Request, Context, Services, Path, Config); _ -> handle_not_found(Request, Context, Services, Config) end. -file("src/dream/controllers/static.gleam", 158). -spec serve_validated_path( dream@http@request:request(), AFHN, AFHO, binary(), binary(), config(AFHN, AFHO) ) -> dream@http@response:response(). serve_validated_path(Request, Context, Services, Root, Filepath, Config) -> Full_path = case Filepath of <<""/utf8>> -> Root; _ -> <<<>/binary, Filepath/binary>> end, case simplifile_erl:is_file(Full_path) of {ok, true} -> serve_file(Full_path); _ -> handle_directory_or_missing( Request, Context, Services, Full_path, Config ) end. -file("src/dream/controllers/static.gleam", 142). ?DOC( " Serve static files from a directory\n" "\n" " Security:\n" " - Prevents path traversal attacks (../)\n" " - Validates paths stay within root directory\n" " - Returns 404 for files outside root\n" "\n" " Usage:\n" " ```gleam\n" " import dream/controllers/static.{default_config, serve}\n" " import dream/http/request.{type Request, get_string_param}\n" " import dream/http/response.{type Response, json_response}\n" " import dream/http/status.{bad_request}\n" " \n" " pub fn serve_public(request: Request, context, services) -> Response {\n" " case get_string_param(request, \"filepath\") {\n" " Ok(filepath) -> \n" " serve(\n" " request: request,\n" " context: context,\n" " services: services,\n" " root: \"./public\",\n" " filepath: filepath,\n" " config: default_config(),\n" " )\n" " Error(msg) -> json_response(bad_request, error_json(msg))\n" " }\n" " }\n" " ```\n" ). -spec serve( dream@http@request:request(), AFHJ, AFHK, binary(), binary(), config(AFHJ, AFHK) ) -> dream@http@response:response(). serve(Request, Context, Services, Root, Filepath, Config) -> case is_safe_path(Filepath) of false -> handle_not_found(Request, Context, Services, Config); true -> serve_validated_path( Request, Context, Services, Root, Filepath, Config ) end.