Packages

phoenix_kit

1.7.21
1.7.210 1.7.209 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
phoenix_kit lib mix tasks phoenix_kit.migrate_blog_versions.ex
Raw

lib/mix/tasks/phoenix_kit.migrate_blog_versions.ex

defmodule Mix.Tasks.PhoenixKit.MigrateBlogVersions do
# Suppress dialyzer warnings for Mix module functions not recognized at analysis time
@dialyzer :no_undefined_callbacks
@dialyzer {:no_unknown, run: 1}
@moduledoc """
Migrates existing blog posts to the new versioned folder structure.
This task moves blog posts from the legacy flat structure to the versioned structure:
- Legacy: `blog-slug/post-slug/en.phk`
- Versioned: `blog-slug/post-slug/v1/en.phk`
All existing posts are treated as version 1 with the appropriate metadata fields added.
## Usage
mix phoenix_kit.migrate_blog_versions
## Options
--dry-run Show what would be changed without making changes
--verbose Show detailed output during migration
--blog SLUG Only migrate a specific blog (default: all blogs)
## Examples
# See what would be changed
mix phoenix_kit.migrate_blog_versions --dry-run
# Migrate all blogs with detailed output
mix phoenix_kit.migrate_blog_versions --verbose
# Migrate a specific blog
mix phoenix_kit.migrate_blog_versions --blog my-blog
The migration is idempotent - posts already in versioned structure are skipped.
"""
@shortdoc "Migrates blog posts to versioned folder structure"
use Mix.Task
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.Metadata
alias PhoenixKit.Modules.Publishing.Storage
@impl Mix.Task
def run(args) do
{opts, _args, _invalid} =
OptionParser.parse(args,
switches: [
dry_run: :boolean,
verbose: :boolean,
blog: :string
],
aliases: [
d: :dry_run,
v: :verbose,
b: :blog
]
)
# Start the application
Mix.Task.run("app.start", [])
# Welcome message
Mix.shell().info([
:bright,
:blue,
"\n📦 PhoenixKit Blog Version Migration Tool\n",
:normal,
"Migrating blog posts to versioned folder structure\n"
])
if opts[:dry_run] do
Mix.shell().info([:yellow, "🔍 DRY RUN MODE - No files will be modified\n"])
end
{:ok, stats} = run_migration(opts)
display_success_summary(stats, opts)
end
defp run_migration(opts) do
blogs = get_blogs_to_migrate(opts)
if blogs == [] do
Mix.shell().info([:yellow, "No blogs found to migrate."])
{:ok, %{migrated: 0, skipped: 0, errors: 0}}
else
stats = %{migrated: 0, skipped: 0, errors: 0}
stats =
Enum.reduce(blogs, stats, fn blog, acc_stats ->
migrate_blog(blog, opts, acc_stats)
end)
{:ok, stats}
end
end
defp get_blogs_to_migrate(opts) do
all_blogs = Publishing.list_groups()
case opts[:blog] do
nil ->
# Migrate all blogs
all_blogs
slug ->
# Find specific blog
case Enum.find(all_blogs, fn b -> b["slug"] == slug end) do
nil ->
Mix.shell().error("Blog '#{slug}' not found")
[]
blog ->
[blog]
end
end
end
defp migrate_blog(blog, opts, stats) do
blog_slug = blog["slug"]
blog_mode = blog["mode"] || "timestamp"
if opts[:verbose] do
Mix.shell().info("\n📁 Processing blog: #{blog["name"]} (#{blog_slug})")
Mix.shell().info(" Mode: #{blog_mode}")
end
# Only slug-mode blogs support versioning
if blog_mode != "slug" do
if opts[:verbose] do
Mix.shell().info([
:yellow,
" Skipping: Timestamp-mode blogs don't require version migration"
])
end
stats
else
migrate_slug_mode_blog(blog_slug, opts, stats)
end
end
defp migrate_slug_mode_blog(blog_slug, opts, stats) do
blog_path = Storage.group_path(blog_slug)
case File.ls(blog_path) do
{:ok, entries} ->
post_slugs =
entries
|> Enum.filter(&File.dir?(Path.join(blog_path, &1)))
|> Enum.reject(&(String.starts_with?(&1, ".") or String.starts_with?(&1, "_trash")))
if opts[:verbose] do
Mix.shell().info(" Found #{length(post_slugs)} post directories")
end
Enum.reduce(post_slugs, stats, fn post_slug, acc_stats ->
migrate_post(blog_slug, post_slug, opts, acc_stats)
end)
{:error, reason} ->
Mix.shell().error(" Error reading blog directory: #{reason}")
%{stats | errors: stats.errors + 1}
end
end
defp migrate_post(blog_slug, post_slug, opts, stats) do
post_path = Path.join(Storage.group_path(blog_slug), post_slug)
# Check the post structure
case Storage.detect_post_structure(post_path) do
:versioned ->
# Already versioned, skip
if opts[:verbose] do
Mix.shell().info(" ✓ #{post_slug}: Already versioned (skipped)")
end
%{stats | skipped: stats.skipped + 1}
:legacy ->
# Needs migration
if opts[:dry_run] do
Mix.shell().info(" → #{post_slug}: Would migrate to v1/")
%{stats | migrated: stats.migrated + 1}
else
case migrate_legacy_post(blog_slug, post_slug, opts) do
:ok ->
Mix.shell().info(" ✓ #{post_slug}: Migrated to v1/")
%{stats | migrated: stats.migrated + 1}
{:error, reason} ->
Mix.shell().error(" ✗ #{post_slug}: Failed - #{reason}")
%{stats | errors: stats.errors + 1}
end
end
:empty ->
if opts[:verbose] do
Mix.shell().info(" - #{post_slug}: Empty directory (skipped)")
end
%{stats | skipped: stats.skipped + 1}
end
end
defp migrate_legacy_post(blog_slug, post_slug, opts) do
post_path = Path.join(Storage.group_path(blog_slug), post_slug)
v1_path = Path.join(post_path, "v1")
with {:ok, phk_files} <- list_phk_files_for_migration(post_path),
:ok <- File.mkdir_p(v1_path),
:ok <- migrate_phk_files(post_path, v1_path, phk_files, opts) do
:ok
else
{:error, :no_files} -> {:error, "No .phk files found"}
{:error, reason} when is_binary(reason) -> {:error, reason}
{:error, reason} -> {:error, "Failed to create v1 directory: #{inspect(reason)}"}
end
end
defp list_phk_files_for_migration(post_path) do
case File.ls(post_path) do
{:ok, files} ->
phk_files = Enum.filter(files, &String.ends_with?(&1, ".phk"))
if phk_files == [], do: {:error, :no_files}, else: {:ok, phk_files}
{:error, reason} ->
{:error, "Failed to list files: #{inspect(reason)}"}
end
end
defp migrate_phk_files(post_path, v1_path, phk_files, opts) do
results = Enum.map(phk_files, &migrate_file(post_path, v1_path, &1, opts))
if Enum.all?(results, &(&1 == :ok)) do
:ok
else
errors = Enum.filter(results, &match?({:error, _}, &1))
{:error, inspect(errors)}
end
end
defp migrate_file(post_path, v1_path, file, opts) do
source = Path.join(post_path, file)
dest = Path.join(v1_path, file)
with {:ok, content} <- File.read(source),
{:ok, updated_content} <- update_metadata_for_v1(content),
:ok <- File.write(dest, updated_content),
:ok <- File.rm(source) do
if opts[:verbose], do: Mix.shell().info(" Moved: #{file} → v1/#{file}")
:ok
else
{:error, :enoent} -> {:error, "Failed to read: file not found"}
{:error, reason} when is_atom(reason) -> {:error, "File operation failed: #{reason}"}
{:error, reason} -> {:error, reason}
end
end
defp update_metadata_for_v1(content) do
# parse_with_content always returns {:ok, metadata, body_content}
{:ok, metadata, body_content} = Metadata.parse_with_content(content)
# Add version fields if missing
now = DateTime.utc_now() |> DateTime.truncate(:second)
updated_metadata =
metadata
|> Map.put_new(:version, 1)
|> Map.put_new(:version_created_at, DateTime.to_iso8601(now))
|> Map.put_new(:version_created_from, nil)
|> Map.put_new(:is_live, metadata[:status] == "published")
# Serialize back to content
frontmatter = Metadata.serialize(updated_metadata)
updated_content = frontmatter <> "\n" <> body_content
{:ok, updated_content}
end
defp display_success_summary(stats, _opts) do
Mix.shell().info([
:bright,
:green,
"\n✅ Blog version migration completed!\n"
])
Mix.shell().info("📊 Summary:")
Mix.shell().info(" Migrated: #{stats.migrated} posts")
Mix.shell().info(" Skipped: #{stats.skipped} posts (already versioned or empty)")
if stats.errors > 0 do
Mix.shell().info([
:red,
" Errors: #{stats.errors} posts"
])
end
Mix.shell().info([
:bright,
"\n🎉 Posts are now in versioned folder structure!"
])
Mix.shell().info([
:normal,
"\nNext steps:",
"\n • New posts will automatically use v1/ structure",
"\n • Editing a published post will create a new version",
"\n • Use the version switcher in the editor to navigate versions"
])
end
end