From 80da842416c092690564a89103bcaa5144ff1b1c Mon Sep 17 00:00:00 2001
From: Adam Piontek <adam@73k.us>
Date: Wed, 31 Mar 2021 18:57:51 -0400
Subject: [PATCH] initial handling of blog content and serving of blog content
 files/assets

---
 .gitignore                                    |   2 +-
 lib/home73k/application.ex                    |   3 +-
 lib/home73k/blog.ex                           | 116 ++++++++++++++++++
 lib/home73k/repo.ex                           |  43 +++++++
 .../controllers/blog_file_controller.ex       |  30 +++++
 lib/home73k_web/endpoint.ex                   |  12 ++
 lib/home73k_web/router.ex                     |   5 +
 7 files changed, 209 insertions(+), 2 deletions(-)
 create mode 100644 lib/home73k/blog.ex
 create mode 100644 lib/home73k/repo.ex
 create mode 100644 lib/home73k_web/controllers/blog_file_controller.ex

diff --git a/.gitignore b/.gitignore
index 0907e95..40bc87f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,7 +40,7 @@ npm-debug.log
 /.elixir_ls
 
 # blog content repo folder
-/priv/content/*
+/priv/content/
 
 # dev
 TODO.md
diff --git a/lib/home73k/application.ex b/lib/home73k/application.ex
index ecc3a44..ea69fe1 100644
--- a/lib/home73k/application.ex
+++ b/lib/home73k/application.ex
@@ -12,9 +12,10 @@ defmodule Home73k.Application do
       # Start the PubSub system
       {Phoenix.PubSub, name: Home73k.PubSub},
       # Start the Endpoint (http/https)
-      Home73kWeb.Endpoint
+      Home73kWeb.Endpoint,
       # Start a worker by calling: Home73k.Worker.start_link(arg)
       # {Home73k.Worker, arg}
+      Home73k.Blog
     ]
 
     # See https://hexdocs.pm/elixir/Supervisor.html
diff --git a/lib/home73k/blog.ex b/lib/home73k/blog.ex
new file mode 100644
index 0000000..8f25f1c
--- /dev/null
+++ b/lib/home73k/blog.ex
@@ -0,0 +1,116 @@
+defmodule Home73k.Blog do
+  use GenServer
+
+  #
+  # Setup
+  #
+  Application.ensure_all_started(:earmark)
+
+  @repo Home73k.Repo.get()
+
+  #
+  # Client
+  #
+
+  def start_link(_opts) do
+    GenServer.start_link(__MODULE__, %{posts: [], tags: [], files: []}, name: __MODULE__)
+  end
+
+  def get_posts() do
+    GenServer.call(__MODULE__, :get_posts)
+  end
+
+  def get_files() do
+    GenServer.call(__MODULE__, :get_files)
+  end
+
+  # def push(pid, element) do
+  #   GenServer.cast(pid, {:push, element})
+  # end
+
+  # def pop(pid) do
+  #   GenServer.call(pid, :pop)
+  # end
+
+  # def put(server, key, value) do
+  #   GenServer.cast(server, {:put, key, value})
+  # end
+
+  # def get(server, key) do
+  #   GenServer.call(server, {:get, key})
+  # end
+
+  #
+  # Server
+  #
+
+  @impl true
+  def init(state) do
+    repo_all_paths = @repo.path |> Path.join("**/*.*") |> Path.wildcard()
+    repo_post_paths = repo_all_paths |> Enum.filter(fn f -> String.ends_with?(f, ".md") end)
+    repo_file_paths = repo_all_paths |> Enum.filter(fn f -> !String.ends_with?(f, ".md") end)
+    {:ok, %{state | posts: repo_post_paths, files: repo_file_paths}}
+  end
+
+  @impl true
+  def handle_call(:get_posts, _from, %{posts: posts} = state) do
+    {:reply, posts, state}
+  end
+
+  @impl true
+  def handle_call(:get_files, _from, %{files: files} = state) do
+    {:reply, files, state}
+  end
+
+  # @impl true
+  # def handle_call(:pop, _from, [head | tail]) do
+  #   {:reply, head, tail}
+  # end
+
+  # @impl true
+  # def handle_cast({:push, element}, state) do
+  #   {:noreply, [element | state]}
+  # end
+
+  # def handle_cast({:put, key, value}, state) do
+  #   {:noreply, Map.put(state, key, value)}
+  # end
+
+  # def handle_call({:get, key}, _from, state) do
+  #   {:reply, Map.fetch!(state, key), state}
+  # end
+
+  # alias Home73k.Blog.Post
+
+  # @posts_dir Application.compile_env(:home73k, :blog_posts_dir, "posts")
+
+  # posts_paths =
+  #   @posts_dir
+  #   |> Path.join("**/*.md")
+  #   |> Path.wildcard()
+  #   |> Enum.sort()
+
+  # posts =
+  #   for post_path <- posts_paths do
+  #     @external_resource Path.relative_to_cwd(post_path)
+  #     Post.parse!(post_path)
+  #   end
+
+  # @posts Enum.sort_by(posts, & &1.date, {:desc, Date})
+
+  # @tags posts |> Enum.flat_map(& &1.tags) |> Enum.uniq() |> Enum.sort()
+
+  # def list_posts, do: @posts
+  # def list_tags, do: @tags
+
+  # defmodule NotFoundError do
+  #   defexception [:message, plug_status: 404]
+  # end
+
+  # def get_posts_by_tag!(tag) do
+  #   case Enum.filter(list_posts(), &(tag in &1.tags)) do
+  #     [] -> raise NotFoundError, "posts with tag=#{tag} not found"
+  #     posts -> posts
+  #   end
+  # end
+end
diff --git a/lib/home73k/repo.ex b/lib/home73k/repo.ex
new file mode 100644
index 0000000..23bd676
--- /dev/null
+++ b/lib/home73k/repo.ex
@@ -0,0 +1,43 @@
+defmodule Home73k.Repo do
+  @repo_url Application.compile_env(:home73k, [:content_repo, :url], nil)
+  @content_path Application.compile_env(:home73k, [:content_repo, :path], "./priv/content")
+                |> Path.expand()
+
+  ######################################################################
+  # REPO SETUP
+  ######################################################################
+  def get do
+    @content_path |> File.exists?() |> init_repo()
+  end
+
+  def content_path, do: @content_path
+
+  # If content path is absent, clone repo if url is present
+  defp init_repo(false) do
+    if @repo_url do
+      {:ok, repo} = Git.clone([@repo_url, @content_path])
+      repo
+    else
+      nil
+    end
+  end
+
+  # If content path exists, check for .git child and pull or return nil
+  defp init_repo(true) do
+    if git_data_dir_ok?() do
+      Git.new(@content_path)
+    else
+      nil
+    end
+  end
+
+  defp git_data_dir_ok? do
+    git_data_dir = Path.join(@content_path, ".git")
+    File.exists?(git_data_dir) && File.dir?(git_data_dir)
+  end
+
+  ######################################################################
+  # REPO OPERATIONS
+  ######################################################################
+  def update(repo), do: Git.pull(repo)
+end
diff --git a/lib/home73k_web/controllers/blog_file_controller.ex b/lib/home73k_web/controllers/blog_file_controller.ex
new file mode 100644
index 0000000..dc9b910
--- /dev/null
+++ b/lib/home73k_web/controllers/blog_file_controller.ex
@@ -0,0 +1,30 @@
+defmodule Home73kWeb.BlogFileController do
+  use Home73kWeb, :controller
+
+  @moduledoc """
+  This controller handles path requests that didn't match an existing route,
+  including the main Plug.Static for the / root path.
+
+  To handle this situation, we'll check if the requested resource exists as
+  a file under the blog content repo folder, by querying the Blog genserve.
+
+  If it exists, we'll redirect to the blog content static path under /_
+  Otherwise, we'll return 404 not found.
+  """
+
+  alias Home73k.Repo
+  alias Home73k.Blog
+
+  def index(conn, _params) do
+    # What would be the content path of this requested resource?
+    content_path = Repo.content_path() |> Path.join(conn.request_path)
+
+    # Check if it exists in the Blog's known files
+    Blog.get_files()
+    |> Enum.member?(content_path)
+    |> case do
+      true -> redirect(conn, to: "/_#{conn.request_path}")
+      false -> send_resp(conn, 404, "not found")
+    end
+  end
+end
diff --git a/lib/home73k_web/endpoint.ex b/lib/home73k_web/endpoint.ex
index 3e9a938..1559f7c 100644
--- a/lib/home73k_web/endpoint.ex
+++ b/lib/home73k_web/endpoint.ex
@@ -26,6 +26,18 @@ defmodule Home73kWeb.Endpoint do
     gzip: true,
     only: ~w(css fonts images js favicon.ico robots.txt DF185CEE29A3D443_public_key.asc)
 
+  # Blog static path handler
+  if File.dir?(Home73k.Repo.content_path()) do
+    plug Plug.Static,
+      at: "/_",
+      from: Home73k.Repo.content_path(),
+      gzip: false,
+      only:
+        Home73k.Repo.content_path()
+        |> File.ls!()
+        |> Enum.filter(fn f -> !String.starts_with?(f, ".") end)
+  end
+
   # Code reloading can be explicitly enabled under the
   # :code_reloader configuration of your endpoint.
   if code_reloading? do
diff --git a/lib/home73k_web/router.ex b/lib/home73k_web/router.ex
index a2bd4cc..ef374be 100644
--- a/lib/home73k_web/router.ex
+++ b/lib/home73k_web/router.ex
@@ -43,4 +43,9 @@ defmodule Home73kWeb.Router do
       live_dashboard "/dashboard", metrics: Home73kWeb.Telemetry
     end
   end
+
+  # Wildcard path for handling Blog Files from repo
+  scope "/", Home73kWeb do
+    get "/*path", BlogFileController, :index
+  end
 end