fewd/src/fd_sfc_entry.erl
2025-10-22 14:25:59 -07:00

100 lines
2.7 KiB
Erlang

% @doc non-servery functions for static file caching
%
% this spams the filesystem, so it's not "pure" code
-module(fd_sfc_entry).
-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.