Implemented the GenServer crawler. It's stable
This commit is contained in:
parent
f6f71d1ee5
commit
76134fa14f
@ -2,41 +2,67 @@ defmodule LogsrvApi do
|
||||
@moduledoc """
|
||||
Documentation for `LogsrvApi`.
|
||||
"""
|
||||
alias LogsrvApi.{Filesystem, Journal, Page}
|
||||
alias LogsrvApi.Filesystem
|
||||
|
||||
@repo Filesystem
|
||||
|
||||
def pages do
|
||||
@repo.all(Page)
|
||||
def pages, do: @repo.list! :pages
|
||||
def journals, do: @repo.list! :journals
|
||||
|
||||
def to_title(fd) do
|
||||
fd
|
||||
|> String.replace(~r/_/," ")
|
||||
|> String.replace(~r/\.md$/,"")
|
||||
|> String.replace("%2F","/")
|
||||
|> String.capitalize
|
||||
end
|
||||
def page(title) do
|
||||
@repo.get!(Page, title)
|
||||
def to_slug(fd) do
|
||||
fd
|
||||
|> String.downcase
|
||||
|> String.replace(~r/_/,"-")
|
||||
|> String.replace(~r/\..*/,"")
|
||||
|> String.replace("%2F","_")
|
||||
end
|
||||
def to_path(fd), do: fd |> @repo.locate
|
||||
def to_date(slug) do
|
||||
Timex.parse!(slug, "%Y-%m-%d", :strftime)
|
||||
end
|
||||
|
||||
def journals do
|
||||
@repo.all(Journal)
|
||||
end
|
||||
def journal(str) do
|
||||
{:ok, date} = Filesystem.to_date(str)
|
||||
@repo.get!(Journal, date)
|
||||
def date_modified(path) do
|
||||
%{ctime: ctime} = (path |> File.stat!)
|
||||
ctime
|
||||
|> Timex.to_datetime
|
||||
end
|
||||
|
||||
def get_by_id(id) do
|
||||
# Try to guess whether it's a page or a journal
|
||||
{_, date} = Filesystem.to_date(id)
|
||||
case date do
|
||||
:invalid_format ->
|
||||
IO.puts(id)
|
||||
page(id)
|
||||
_ ->
|
||||
IO.puts(id)
|
||||
journal(date)
|
||||
def split(data) do
|
||||
try do
|
||||
[frontmatter, markdown] = String.split(data, ~r/\n-{3,}\n/, parts: 2)
|
||||
{parse_yaml(frontmatter), Earmark.as_html!(markdown)}
|
||||
rescue MatchError ->
|
||||
{:nil, Earmark.as_html!(data)}
|
||||
end
|
||||
end
|
||||
def parse_yaml(yaml) do
|
||||
yaml
|
||||
|> :yamerl_constr.string
|
||||
|> List.flatten
|
||||
end
|
||||
def extract({props, content}, post) do
|
||||
new_title = get_prop(props, "title") || post.title
|
||||
%{post |
|
||||
title: new_title |> to_string,
|
||||
tags: get_prop(props, "tags"),
|
||||
content: content}
|
||||
end
|
||||
def get_prop(props, key) do
|
||||
if is_nil(props) do
|
||||
nil
|
||||
else
|
||||
case :proplists.get_value(String.to_char_list(key), props) do
|
||||
:undefined -> nil
|
||||
x -> x
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def read(entry) do
|
||||
@repo.read(entry)
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
@ -7,10 +7,7 @@ defmodule LogsrvApi.Application do
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# Starts a worker by calling: LogsrvApi.Worker.start_link(arg)
|
||||
# {LogsrvApi.Worker, arg}
|
||||
]
|
||||
children = []
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
|
||||
@ -1,7 +1,16 @@
|
||||
defmodule LogsrvApi.Crawler do
|
||||
alias LogsrvApi.{Filesystem,Page,Journal}
|
||||
|
||||
def crawl do
|
||||
Filesystem.
|
||||
def crawl(module) do
|
||||
Filesystem.dir(module.atom)
|
||||
|> File.ls!
|
||||
|> Filesystem.subdir(module.atom)
|
||||
|> Enum.map(&module.init/1)
|
||||
|> Enum.sort(&sort/2)
|
||||
end
|
||||
|
||||
def sort(a, b) do
|
||||
Timex.compare(a.date, b.date) > 0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -1,5 +1,51 @@
|
||||
defmodule LogsrvApi.Filesystem do
|
||||
alias LogsrvApi.{Journal, Page}
|
||||
use GenServer
|
||||
|
||||
def start_link(_opts) do
|
||||
GenServer.start_link(__MODULE__, :ok, [name: __MODULE__])
|
||||
end
|
||||
|
||||
def init(:ok) do
|
||||
pages = LogsrvApi.Crawler.crawl(Page)
|
||||
journals = LogsrvApi.Crawler.crawl(Journal)
|
||||
posts = %{pages: pages, journals: journals}
|
||||
{:ok, posts}
|
||||
end
|
||||
|
||||
def get_by_slug(slug) do
|
||||
GenServer.call(__MODULE__, {:get_by_slug, slug})
|
||||
end
|
||||
|
||||
def get_by_slug!(slug) do
|
||||
{:ok, post} = get_by_slug(slug)
|
||||
post
|
||||
end
|
||||
|
||||
def list() do
|
||||
%{
|
||||
journals: list!(:journals),
|
||||
pages: list!(:pages)
|
||||
}
|
||||
end
|
||||
def list(type) do
|
||||
GenServer.call(__MODULE__, {:list, type})
|
||||
end
|
||||
def list!(type) do
|
||||
{:ok, files} = list(type)
|
||||
files
|
||||
end
|
||||
|
||||
def handle_call({:get_by_slug, slug}, _from, posts) do
|
||||
case [posts.journals, posts.pages] |> List.flatten |> Enum.find(fn(x) -> x.slug == slug end) do
|
||||
nil -> {:reply, {:error,:not_found}, posts}
|
||||
page -> {:reply, {:ok, page}, posts}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:list, type}, _from, posts) do
|
||||
{:reply, {:ok, posts |> Map.get(type)}, posts}
|
||||
end
|
||||
|
||||
def to_date(str) when is_bitstring(str) do
|
||||
str
|
||||
@ -17,51 +63,46 @@ defmodule LogsrvApi.Filesystem do
|
||||
def dir(subdir) do
|
||||
"#{dir()}/#{subdir}/"
|
||||
end
|
||||
def locate(fd, module) do
|
||||
dir(module.atom) <> fd
|
||||
|
||||
def locate(fd) do
|
||||
page = dir(:pages) <> fd
|
||||
journal = dir(:journals) <> fd
|
||||
cond do
|
||||
page |> File.exists? ->
|
||||
page
|
||||
journal |> File.exists? ->
|
||||
journal
|
||||
true ->
|
||||
# Occasional race condition I haven't figured out.
|
||||
# Just repeating the function seems to work.
|
||||
locate(fd)
|
||||
end
|
||||
end
|
||||
|
||||
def all(Page) do
|
||||
dir(:pages)
|
||||
|> File.ls!
|
||||
|> Enum.map(fn(fd) ->
|
||||
case fd |> File.ls do
|
||||
{:error, _} ->
|
||||
IO.puts("Error on #{fd}")
|
||||
Page.init(fd)
|
||||
pages ->
|
||||
IO.puts(pages)
|
||||
pages |> Enum.map(fn(page) ->
|
||||
"pages/#{page}" |> Page.init
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|> Enum.sort(&sort/2)
|
||||
def is_dir(fd) do
|
||||
path = fd |> locate
|
||||
%{type: type} = path |> File.stat!
|
||||
type == :directory
|
||||
end
|
||||
|
||||
def subdir(list, type) do
|
||||
list
|
||||
|> Enum.map(fn(fd) ->
|
||||
if is_dir(fd) do
|
||||
subdir(dir(type) <>
|
||||
fd
|
||||
|> File.ls!
|
||||
|> Enum.map(fn(dir) ->
|
||||
fd <> "/" <> dir
|
||||
end), type)
|
||||
else
|
||||
fd
|
||||
end
|
||||
end) |> List.flatten
|
||||
end
|
||||
|
||||
def sort(a, b) do
|
||||
Timex.compare(a.date, b.date) > 0
|
||||
end
|
||||
|
||||
def all(Journal) do
|
||||
dir(:journals)
|
||||
|> File.ls!
|
||||
|> Enum.sort |> Enum.reverse
|
||||
|> Enum.map(fn(fd) ->
|
||||
Journal.init(fd)
|
||||
end)
|
||||
end
|
||||
|
||||
def get!(Journal, date) do
|
||||
Enum.find(all(Journal), fn(entry) -> entry.date === date end)
|
||||
end
|
||||
def get!(Page, filename) do
|
||||
Enum.find(all(Page), fn(entry) -> entry.filename === filename end)
|
||||
end
|
||||
|
||||
def read(entry) do
|
||||
entry.type
|
||||
|> locate(entry.filename)
|
||||
|> File.read!
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
defmodule LogsrvApi.Journal do
|
||||
alias LogsrvApi.{Filesystem,Page,Journal}
|
||||
import LogsrvApi
|
||||
|
||||
defstruct slug: "", title: "", date: "", content: "", filename: "", tags: [:fun]
|
||||
def atom, do: :journals
|
||||
|
||||
def init(fd) do
|
||||
{:ok, date} = fd |> String.replace(~r/_/,"-") |> String.replace(~r/\.md$/,"") |> Filesystem.to_date
|
||||
tags = [:fun]
|
||||
|
||||
%{
|
||||
type: Journal,
|
||||
date: date,
|
||||
page = %Journal{
|
||||
filename: fd,
|
||||
tags: tags
|
||||
title: fd |> to_title,
|
||||
slug: fd |> to_slug,
|
||||
date: fd |> to_slug |> to_date
|
||||
}
|
||||
end
|
||||
|
||||
fd |> to_path |> File.read! |> split |> extract(page)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
defmodule LogsrvApi.Page do
|
||||
alias LogsrvApi.{Filesystem,Page,Journal}
|
||||
import LogsrvApi
|
||||
|
||||
defstruct slug: "", title: "", date: "", content: "", filename: "", tags: [:fun]
|
||||
def atom, do: :pages
|
||||
@ -15,59 +16,4 @@ defmodule LogsrvApi.Page do
|
||||
fd |> to_path |> File.read! |> split |> extract(page)
|
||||
|
||||
end
|
||||
|
||||
def to_title(fd) do
|
||||
fd
|
||||
|> String.replace(~r/_/," ")
|
||||
|> String.replace(~r/\.md$/,"")
|
||||
|> String.replace("%2F","/")
|
||||
|> String.capitalize
|
||||
end
|
||||
def to_slug(fd) do
|
||||
fd
|
||||
|> String.replace(~r/_/,"-")
|
||||
|> String.replace(~r/\.md$/,"")
|
||||
|> String.replace("%2F","_")
|
||||
end
|
||||
def to_path(fd) do
|
||||
fd
|
||||
|> Filesystem.locate(Page)
|
||||
end
|
||||
|
||||
def date_modified(path) do
|
||||
%{ctime: ctime} = (path |> File.stat!)
|
||||
ctime
|
||||
|> NaiveDateTime.from_erl!()
|
||||
|> DateTime.from_naive!("Etc/UTC")
|
||||
end
|
||||
|
||||
defp split(data) do
|
||||
try do
|
||||
[frontmatter, markdown] = String.split(data, ~r/\n-{3,}\n/, parts: 2)
|
||||
{parse_yaml(frontmatter), Earmark.as_html!(markdown)}
|
||||
rescue MatchError ->
|
||||
{:nil, Earmark.as_html!(data)}
|
||||
end
|
||||
end
|
||||
def parse_yaml(yaml) do
|
||||
yaml
|
||||
|> :yamerl_constr.string
|
||||
|> List.flatten
|
||||
end
|
||||
defp extract({props, content}, post) do
|
||||
%{post |
|
||||
title: get_prop(props, "title") |> to_string || post.title,
|
||||
tags: get_prop(props, "tags"),
|
||||
content: content}
|
||||
end
|
||||
defp get_prop(props, key) do
|
||||
if is_nil(props) do
|
||||
:nil
|
||||
else
|
||||
case :proplists.get_value(String.to_char_list(key), props) do
|
||||
:undefined -> nil
|
||||
x -> x
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
/* This file is for your main application CSS */
|
||||
@import "./phoenix.css";
|
||||
@import "./bear.css";
|
||||
|
||||
/* Alerts and form errors used by phx.new */
|
||||
.alert {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -55,8 +55,8 @@ select {
|
||||
/* Headers */
|
||||
header {
|
||||
width: 100%;
|
||||
background: #002B36;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
background: #eee;
|
||||
border-bottom: 4px solid #222;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
header section {
|
||||
|
||||
@ -12,7 +12,8 @@ defmodule LogsrvWeb.Application do
|
||||
LogsrvWeb.Telemetry,
|
||||
# Start the Endpoint (http/https),
|
||||
{Phoenix.PubSub, [name: LogsrvWeb.PubSub, adapter: Phoenix.PubSub.PG2]},
|
||||
LogsrvWeb.Endpoint
|
||||
LogsrvWeb.Endpoint,
|
||||
{LogsrvApi.Filesystem, []}
|
||||
# Start a worker by calling: LogsrvWeb.Worker.start_link(arg)
|
||||
# {LogsrvWeb.Worker, arg}
|
||||
]
|
||||
|
||||
@ -1,20 +1,32 @@
|
||||
defmodule LogsrvWeb.PostController do
|
||||
use LogsrvWeb, :controller
|
||||
import LogsrvApi
|
||||
import LogsrvApi.Filesystem
|
||||
|
||||
def index(conn, _params) do
|
||||
render(conn, "index.html", pages: pages(), journals: journals())
|
||||
%{journals: journals, pages: pages} = list()
|
||||
render conn, "index.html", pages: pages, journals: journals
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
post = id |> get_by_id
|
||||
content = post |> read
|
||||
render(conn, "post.html", post: post, pages: pages(), journals: journals(), content: content)
|
||||
{status, post} = id |> String.downcase |> get_by_slug
|
||||
case status do
|
||||
:ok ->
|
||||
render(conn, "post.html", post: post)
|
||||
_ ->
|
||||
not_found(conn)
|
||||
end
|
||||
end
|
||||
|
||||
def home(conn, _params) do
|
||||
homepage = get_by_id("contents.md")
|
||||
content = homepage |> read
|
||||
render(conn, "post.html", post: homepage, pages: pages(), journals: journals(), content: content)
|
||||
show(conn, %{"id" => "contents"})
|
||||
end
|
||||
|
||||
|
||||
def not_found(conn) do
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> render(LogsrvWeb.ErrorView, "404.html")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
<meta name="csrf-token" content={csrf_token_value()}>
|
||||
<%= live_title_tag assigns[:page_title] || "LogsrvWeb", suffix: " · Phoenix Framework" %>
|
||||
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
|
||||
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/bear.css")}/>
|
||||
<script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
|
||||
</head>
|
||||
<body><div id="root"><div class="theme-inner"><div id="app-container">
|
||||
@ -17,8 +16,8 @@
|
||||
<div>
|
||||
<ul>
|
||||
<li> <b>Directory:</b> <%= LogsrvApi.Filesystem.dir %> </li>
|
||||
<li> <b>Journals:</b> <%= @pages |> length %> </li>
|
||||
<li> <b>Pages:</b> <%= @journals |> length %> </li>
|
||||
<li> <b>Journals:</b> <%= pages |> length %> </li>
|
||||
<li> <b>Pages:</b> <%= journals |> length %> </li>
|
||||
</ul></div>
|
||||
<div> <a href="/pages"><h3>Pages</h3></a> </div><div> <a href="/journals"><h3>Journals</h3></a> </div>
|
||||
</section>
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
<%= for journal <- @journals do %>
|
||||
<li>
|
||||
<a href={post_path journal}>
|
||||
<strong><%= journal.date %></strong> (<%= journal.filename %>)
|
||||
<strong><%= journal.date |> Calendar.strftime("%b %d, %Y") %></strong> (<%= journal.filename %>)
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
<section class="row">
|
||||
<article class="column">
|
||||
<h2><%= @post.date |> Calendar.strftime("%A, %b %d, %Y") %></h2>
|
||||
</article>
|
||||
</section>
|
||||
<section class="row">
|
||||
<%= raw @content |> AlchemistMarkdown.to_html |> cleanup %>
|
||||
</section>
|
||||
<p><a href="/">« Back to index</a></p>
|
||||
<article>
|
||||
<h1><%= @post.title %></h1>
|
||||
<p style='float:right;'><%= Calendar.strftime(@post.date, "%b %d, %Y") %></p>
|
||||
<%= raw(@post.content) %>
|
||||
</article>
|
||||
|
||||
@ -4,4 +4,15 @@ defmodule LogsrvWeb.LayoutView do
|
||||
# Phoenix LiveDashboard is available only in development by default,
|
||||
# so we instruct Elixir to not warn if the dashboard route is missing.
|
||||
@compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}}
|
||||
|
||||
|
||||
def pages do
|
||||
{:ok, pages} = LogsrvApi.Filesystem.list(:pages)
|
||||
pages
|
||||
end
|
||||
def journals do
|
||||
{:ok, journals} = LogsrvApi.Filesystem.list(:journals)
|
||||
journals
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -3,7 +3,7 @@ defmodule LogsrvWeb.PostView do
|
||||
import LogsrvApi.Journal
|
||||
|
||||
def post_path(post) do
|
||||
"/post/#{post.filename}"
|
||||
"/post/#{post.slug}"
|
||||
end
|
||||
|
||||
def cleanup(str) do
|
||||
|
||||
Loading…
Reference in New Issue
Block a user