Packages
phoenix_kit
1.7.43
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/publishing/storage/versions.ex
defmodule PhoenixKit.Modules.Publishing.Storage.Versions do
@moduledoc """
Version management for publishing storage.
Handles version detection, listing, status management,
and version creation operations for posts.
"""
alias PhoenixKit.Modules.Publishing.Metadata
alias PhoenixKit.Modules.Publishing.Storage.Languages
alias PhoenixKit.Modules.Publishing.Storage.Paths
@doc """
Detects whether a post directory uses versioned structure (v1/, v2/, etc.)
or legacy structure (files directly in post directory).
Returns:
- `:versioned` if v1/ or any vN/ directory exists
- `:legacy` if .phk files exist directly in the directory
- `:empty` if neither exists
"""
@spec detect_post_structure(String.t()) :: :versioned | :legacy | :empty
def detect_post_structure(post_path) do
if File.dir?(post_path) do
case File.ls(post_path) do
{:ok, entries} ->
has_version_dirs = Enum.any?(entries, &version_dir?/1)
has_phk_files = Enum.any?(entries, &String.ends_with?(&1, ".phk"))
cond do
has_version_dirs -> :versioned
has_phk_files -> :legacy
true -> :empty
end
{:error, _} ->
:empty
end
else
:empty
end
end
@doc """
Check if a directory name matches version pattern (v1, v2, etc.)
"""
@spec version_dir?(String.t()) :: boolean()
def version_dir?(name) do
Regex.match?(~r/^v\d+$/, name)
end
@doc """
Lists all version numbers for a slug-mode post.
Returns sorted list of integers (e.g., [1, 2, 3]).
For legacy posts without version directories, returns [1].
"""
@spec list_versions(String.t(), String.t()) :: [integer()]
def list_versions(group_slug, post_slug) do
post_path = Path.join([Paths.group_path(group_slug), post_slug])
case detect_post_structure(post_path) do
:versioned ->
case File.ls(post_path) do
{:ok, entries} ->
entries
|> Enum.filter(&version_dir?/1)
|> Enum.map(&parse_version_number/1)
|> Enum.reject(&is_nil/1)
|> Enum.sort()
{:error, _} ->
[]
end
:legacy ->
[1]
:empty ->
[]
end
end
@doc """
Parse version number from directory name (e.g., "v2" -> 2)
"""
@spec parse_version_number(String.t()) :: integer() | nil
def parse_version_number("v" <> num_str) do
case Integer.parse(num_str) do
{num, ""} -> num
_ -> nil
end
end
def parse_version_number(_), do: nil
@doc """
Gets the latest (highest) version number for a post.
"""
@spec get_latest_version(String.t(), String.t()) :: {:ok, integer()} | {:error, :not_found}
def get_latest_version(group_slug, post_slug) do
case list_versions(group_slug, post_slug) do
[] -> {:error, :not_found}
versions -> {:ok, Enum.max(versions)}
end
end
@doc """
Gets the latest published version number for a post.
Checks each version's primary language file for status.
"""
@spec get_latest_published_version(String.t(), String.t()) ::
{:ok, integer()} | {:error, :not_found}
def get_latest_published_version(group_slug, post_slug) do
versions = list_versions(group_slug, post_slug)
primary_language = Languages.get_post_primary_language(group_slug, post_slug, nil)
published_version =
versions
|> Enum.sort(:desc)
|> Enum.find(fn version ->
case get_version_status(group_slug, post_slug, version, primary_language) do
"published" -> true
_ -> false
end
end)
case published_version do
nil -> {:error, :not_found}
version -> {:ok, version}
end
end
@doc """
Gets the published version number for a post.
Only ONE version can have status: "published" at a time in the variant versioning model.
Falls back to checking legacy is_live field for unmigrated posts.
"""
@spec get_published_version(String.t(), String.t()) :: {:ok, integer()} | {:error, :not_found}
def get_published_version(group_slug, post_slug) do
versions = list_versions(group_slug, post_slug)
primary_language = Languages.get_post_primary_language(group_slug, post_slug, nil)
published_version =
Enum.find(versions, fn version ->
case get_version_metadata(group_slug, post_slug, version, primary_language) do
{:ok, metadata} -> Map.get(metadata, :status) == "published"
_ -> false
end
end)
# Fall back to legacy is_live for unmigrated posts
published_version =
published_version ||
Enum.find(versions, fn version ->
case get_version_metadata(group_slug, post_slug, version, primary_language) do
{:ok, metadata} -> Map.get(metadata, :legacy_is_live) == true
_ -> false
end
end)
case published_version do
nil -> {:error, :not_found}
version -> {:ok, version}
end
end
@doc """
Deprecated: Use get_published_version/2 instead.
"""
@deprecated "Use get_published_version/2 instead"
@spec get_live_version(String.t(), String.t()) :: {:ok, integer()} | {:error, :not_found}
def get_live_version(group_slug, post_slug) do
get_published_version(group_slug, post_slug)
end
@doc """
Gets the status of a specific version for a language.
"""
@spec get_version_status(String.t(), String.t(), integer(), String.t()) :: String.t() | nil
def get_version_status(group_slug, post_slug, version, language) do
case get_version_metadata(group_slug, post_slug, version, language) do
{:ok, metadata} -> Map.get(metadata, :status)
_ -> nil
end
end
@doc """
Gets the metadata for a specific version and language.
"""
@spec get_version_metadata(String.t(), String.t(), integer(), String.t()) ::
{:ok, map()} | {:error, :not_found}
def get_version_metadata(group_slug, post_slug, version, language) do
post_path = Path.join([Paths.group_path(group_slug), post_slug])
file_path =
case detect_post_structure(post_path) do
:versioned ->
Path.join([post_path, "v#{version}", Languages.language_filename(language)])
:legacy when version == 1 ->
Path.join([post_path, Languages.language_filename(language)])
_ ->
nil
end
if file_path && File.exists?(file_path) do
case File.read(file_path) do
{:ok, content} ->
{:ok, metadata, _body} = Metadata.parse_with_content(content)
{:ok, metadata}
{:error, _} ->
{:error, :not_found}
end
else
{:error, :not_found}
end
end
@doc """
Returns the version directory path for a post.
"""
@spec version_path(String.t(), String.t(), integer()) :: String.t()
def version_path(group_slug, post_slug, version) do
Path.join([Paths.group_path(group_slug), post_slug, "v#{version}"])
end
@doc """
Loads version statuses for all versions of a post.
Returns a map of version number => status.
"""
@spec load_version_statuses(String.t(), String.t(), [integer()], String.t() | nil) ::
%{integer() => String.t()}
def load_version_statuses(group_slug, post_slug, versions, primary_language \\ nil) do
primary_lang = primary_language || Languages.get_primary_language()
Enum.reduce(versions, %{}, fn version, acc ->
status = get_version_status(group_slug, post_slug, version, primary_lang)
Map.put(acc, version, status)
end)
end
@doc """
Loads version_created_at dates for all specified versions.
Returns a map of version number => ISO 8601 date string.
"""
@spec load_version_dates(String.t(), String.t(), [integer()], String.t() | nil) :: %{
integer() => String.t() | nil
}
def load_version_dates(group_slug, post_slug, versions, primary_language \\ nil) do
primary_lang = primary_language || Languages.get_primary_language()
Enum.reduce(versions, %{}, fn version, acc ->
date = get_version_date(group_slug, post_slug, version, primary_lang)
Map.put(acc, version, date)
end)
end
@doc """
Loads available languages for each version of a post.
Returns a map of version number => list of language codes.
This is useful for showing which translations exist for a specific version,
e.g., to display the published version's translations in the listing.
"""
@spec load_version_languages(String.t(), String.t(), [integer()]) :: %{
integer() => [String.t()]
}
def load_version_languages(group_slug, post_slug, versions) do
post_path = Path.join([Paths.group_path(group_slug), post_slug])
structure = detect_post_structure(post_path)
Enum.reduce(versions, %{}, fn version, acc ->
languages = get_version_languages(post_path, structure, version)
Map.put(acc, version, languages)
end)
end
# Gets the available languages for a specific version
defp get_version_languages(post_path, :versioned, version) do
version_dir = Path.join(post_path, "v#{version}")
Languages.detect_available_languages(version_dir)
end
defp get_version_languages(post_path, :legacy, 1) do
Languages.detect_available_languages(post_path)
end
defp get_version_languages(_post_path, _structure, _version), do: []
@doc """
Gets the version_created_at date for a specific version.
"""
@spec get_version_date(String.t(), String.t(), integer(), String.t()) :: String.t() | nil
def get_version_date(group_slug, post_slug, version, language) do
case get_version_metadata(group_slug, post_slug, version, language) do
{:ok, metadata} -> Map.get(metadata, :version_created_at)
_ -> nil
end
end
@doc """
Resolves which version directory to read from.
"""
@spec resolve_version_dir(
String.t(),
:versioned | :legacy | :empty,
integer() | nil,
String.t(),
String.t()
) ::
{:ok, integer(), String.t()} | {:error, atom()}
def resolve_version_dir(post_dir, :versioned, nil, group_slug, post_slug) do
case get_latest_version(group_slug, post_slug) do
{:ok, version} ->
{:ok, version, Path.join(post_dir, "v#{version}")}
{:error, _} ->
{:error, :no_versions}
end
end
def resolve_version_dir(post_dir, :versioned, version, _group_slug, _post_slug) do
version_dir = Path.join(post_dir, "v#{version}")
if File.dir?(version_dir) do
{:ok, version, version_dir}
else
{:error, :version_not_found}
end
end
def resolve_version_dir(post_dir, :legacy, _version, _group_slug, _post_slug) do
{:ok, 1, post_dir}
end
def resolve_version_dir(_post_dir, :empty, _version, _group_slug, _post_slug) do
{:error, :not_found}
end
@doc """
Resolves the version directory for listing operations.
Returns {:ok, version, content_dir} or {:error, reason}.
"""
@spec resolve_version_dir_for_listing(
String.t(),
:versioned | :legacy | :empty,
String.t(),
String.t()
) ::
{:ok, integer(), String.t()} | {:error, atom()}
def resolve_version_dir_for_listing(post_path, :versioned, group_slug, post_slug) do
case get_latest_version(group_slug, post_slug) do
{:ok, version} ->
{:ok, version, Path.join(post_path, "v#{version}")}
{:error, _} ->
{:error, :no_versions}
end
end
def resolve_version_dir_for_listing(post_path, :legacy, _group_slug, _post_slug) do
{:ok, 1, post_path}
end
def resolve_version_dir_for_listing(_post_path, :empty, _group_slug, _post_slug) do
{:error, :empty}
end
@doc """
Checks if a version is the published (live) version.
"""
@spec version_is_published?(String.t(), String.t(), integer()) :: boolean()
def version_is_published?(group_slug, post_identifier, version) do
case get_published_version(group_slug, post_identifier) do
{:ok, ^version} -> true
_ -> false
end
end
@doc """
Checks if a post has only one version.
"""
@spec only_version?(String.t()) :: boolean()
def only_version?(post_dir) do
case File.ls(post_dir) do
{:ok, entries} ->
version_dirs = Enum.filter(entries, &version_dir?/1)
length(version_dirs) <= 1
_ ->
true
end
end
end