147 lines
5.2 KiB
Elixir
147 lines
5.2 KiB
Elixir
defmodule BDS.Posts.FileSync do
|
|
@moduledoc false
|
|
|
|
alias BDS.Frontmatter
|
|
alias BDS.Persistence
|
|
alias BDS.Posts.Post
|
|
alias BDS.Posts.Translation
|
|
alias BDS.Projects
|
|
|
|
@doc "Compute the canonical relative path for a published post."
|
|
@spec post_relative_path(String.t(), integer()) :: String.t()
|
|
def post_relative_path(slug, created_at) do
|
|
datetime = Persistence.from_unix_ms!(created_at)
|
|
year = Integer.to_string(datetime.year)
|
|
month = datetime.month |> Integer.to_string() |> String.pad_leading(2, "0")
|
|
Path.join(["posts", year, month, "#{slug}.md"])
|
|
end
|
|
|
|
@doc "Compute the canonical relative path for a translation file."
|
|
@spec translation_relative_path(Post.t(), String.t()) :: String.t()
|
|
def translation_relative_path(post, language) do
|
|
datetime = Persistence.from_unix_ms!(post.created_at)
|
|
year = Integer.to_string(datetime.year)
|
|
month = datetime.month |> Integer.to_string() |> String.pad_leading(2, "0")
|
|
Path.join(["posts", year, month, "#{post.slug}.#{language}.md"])
|
|
end
|
|
|
|
@doc "Resolve the body to publish for a post, falling back to its existing file."
|
|
@spec publishable_post_body(Post.t(), String.t(), term()) :: String.t()
|
|
def publishable_post_body(%Post{content: content}, _full_path, _project)
|
|
when is_binary(content), do: content
|
|
|
|
def publishable_post_body(%Post{file_path: file_path} = post, full_path, project) do
|
|
source_path =
|
|
if file_path in [nil, ""] do
|
|
full_path
|
|
else
|
|
Path.join(Projects.project_data_dir(project), file_path)
|
|
end
|
|
|
|
published_post_body(post, source_path)
|
|
end
|
|
|
|
@doc "Read the body of a previously-published post (DB content first, file fallback)."
|
|
@spec published_post_body(Post.t(), String.t()) :: String.t()
|
|
def published_post_body(%Post{content: content}, _full_path) when is_binary(content),
|
|
do: content
|
|
|
|
def published_post_body(_post, full_path), do: read_markdown_body(full_path)
|
|
|
|
@doc "Read the body section (after frontmatter) from a markdown file on disk."
|
|
@spec read_markdown_body(String.t()) :: String.t()
|
|
def read_markdown_body(path) do
|
|
case File.read(path) do
|
|
{:ok, contents} ->
|
|
case String.split(contents, "\n---\n", parts: 2) do
|
|
[_frontmatter, body] -> String.trim_trailing(body, "\n")
|
|
_parts -> ""
|
|
end
|
|
|
|
{:error, _reason} ->
|
|
""
|
|
end
|
|
end
|
|
|
|
@doc "Serialize a post to a frontmatter+body string for the published file."
|
|
@spec serialize_post_file(Post.t(), integer()) :: String.t()
|
|
def serialize_post_file(post, published_at) do
|
|
Frontmatter.serialize_document(
|
|
[
|
|
{"id", post.id},
|
|
{"title", post.title},
|
|
{"slug", post.slug},
|
|
{"excerpt", post.excerpt},
|
|
{"status", :published},
|
|
{"author", post.author},
|
|
{"language", post.language},
|
|
{"doNotTranslate", post.do_not_translate},
|
|
{"templateSlug", post.template_slug},
|
|
{"createdAt", post.created_at},
|
|
{"updatedAt", post.updated_at},
|
|
{"publishedAt", published_at},
|
|
{"tags", post.tags || []},
|
|
{"categories", post.categories || []}
|
|
],
|
|
post.content
|
|
)
|
|
end
|
|
|
|
@doc "Serialize a translation row to a frontmatter+body string."
|
|
@spec serialize_translation_file(Translation.t(), integer()) :: String.t()
|
|
def serialize_translation_file(translation, published_at) do
|
|
Frontmatter.serialize_document(
|
|
[
|
|
{"id", translation.id},
|
|
{"translationFor", translation.translation_for},
|
|
{"language", translation.language},
|
|
{"title", translation.title},
|
|
{"excerpt", translation.excerpt},
|
|
{"status", :published},
|
|
{"createdAt", translation.created_at},
|
|
{"updatedAt", translation.updated_at},
|
|
{"publishedAt", published_at}
|
|
],
|
|
translation.content
|
|
)
|
|
end
|
|
|
|
@doc "Resolve the body of a translation, falling back to its existing file."
|
|
@spec publishable_translation_body(Translation.t(), String.t()) :: String.t()
|
|
def publishable_translation_body(%Translation{content: content}, _full_path)
|
|
when is_binary(content), do: content
|
|
|
|
def publishable_translation_body(_translation, full_path) do
|
|
read_markdown_body(full_path)
|
|
end
|
|
|
|
@doc "Delete a published post's file on disk (no-op if it has none)."
|
|
@spec delete_post_file(Post.t()) :: :ok | {:error, term()}
|
|
def delete_post_file(%Post{file_path: file_path}) when file_path in [nil, ""], do: :ok
|
|
|
|
def delete_post_file(%Post{} = post) do
|
|
project = Projects.get_project!(post.project_id)
|
|
full_path = Path.join(Projects.project_data_dir(project), post.file_path)
|
|
rm_quiet(full_path)
|
|
end
|
|
|
|
@doc "Delete a translation's file on disk (no-op if it has none)."
|
|
@spec delete_translation_file(Translation.t()) :: :ok | {:error, term()}
|
|
def delete_translation_file(%Translation{file_path: file_path}) when file_path in [nil, ""],
|
|
do: :ok
|
|
|
|
def delete_translation_file(%Translation{} = translation) do
|
|
project = Projects.get_project!(translation.project_id)
|
|
full_path = Path.join(Projects.project_data_dir(project), translation.file_path)
|
|
rm_quiet(full_path)
|
|
end
|
|
|
|
defp rm_quiet(full_path) do
|
|
case File.rm(full_path) do
|
|
:ok -> :ok
|
|
{:error, :enoent} -> :ok
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
end
|