chore: extraction and centralization of utility functions

This commit is contained in:
2026-05-01 17:04:21 +02:00
parent a95e9482a7
commit 79ee67c2e0
15 changed files with 272 additions and 254 deletions

View File

@@ -9,12 +9,7 @@ defmodule BDS.Generation.Progress do
@doc "Extract the `:on_progress` callback from a keyword list of options."
@spec callback(keyword()) :: callback()
def callback(opts) do
case Keyword.get(opts, :on_progress) do
cb when is_function(cb, 2) -> cb
_other -> nil
end
end
def callback(opts), do: BDS.ProgressReporter.callback(opts)
@spec report_generation_started(callback(), non_neg_integer(), String.t()) :: :ok
def report_generation_started(nil, _total, _label), do: :ok
@@ -29,7 +24,8 @@ defmodule BDS.Generation.Progress do
:ok
end
@spec report_generation_progress(callback(), non_neg_integer(), non_neg_integer(), String.t()) :: :ok
@spec report_generation_progress(callback(), non_neg_integer(), non_neg_integer(), String.t()) ::
:ok
def report_generation_progress(nil, _current, _total, _label), do: :ok
def report_generation_progress(_callback, _current, 0, _label), do: :ok
@@ -46,7 +42,8 @@ defmodule BDS.Generation.Progress do
:ok
end
@spec report_validation_snapshot_progress(callback(), atom(), non_neg_integer(), integer()) :: :ok
@spec report_validation_snapshot_progress(callback(), atom(), non_neg_integer(), integer()) ::
:ok
def report_validation_snapshot_progress(nil, _stage, _current, _total), do: :ok
def report_validation_snapshot_progress(_callback, _stage, _current, total)
@@ -75,7 +72,8 @@ defmodule BDS.Generation.Progress do
:ok
end
@spec report_snapshot_stage_progress(stage_callback(), atom(), non_neg_integer(), integer()) :: :ok
@spec report_snapshot_stage_progress(stage_callback(), atom(), non_neg_integer(), integer()) ::
:ok
def report_snapshot_stage_progress(nil, _stage, _current, _total), do: :ok
def report_snapshot_stage_progress(_callback, _stage, _current, total) when total <= 0, do: :ok

24
lib/bds/map_utils.ex Normal file
View File

@@ -0,0 +1,24 @@
defmodule BDS.MapUtils do
@moduledoc false
@typedoc "An attribute map that may use atom or string keys."
@type attrs :: %{optional(atom()) => term(), optional(String.t()) => term()}
@spec attr(attrs(), atom()) :: term()
def attr(attrs, key) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> nil
end
end
@spec maybe_put(map(), term(), term()) :: map()
def maybe_put(map, _key, nil), do: map
def maybe_put(map, key, value), do: Map.put(map, key, value)
@spec blank_to_nil(term()) :: term()
def blank_to_nil(nil), do: nil
def blank_to_nil(""), do: nil
def blank_to_nil(value), do: value
end

View File

@@ -22,8 +22,7 @@ defmodule BDS.MCP.Util do
def normalize_term(value), do: value |> to_string() |> String.downcase()
@spec maybe_put(map(), term(), term()) :: map()
def maybe_put(map, _key, nil), do: map
def maybe_put(map, key, value), do: Map.put(map, key, value)
def maybe_put(map, key, value), do: BDS.MapUtils.maybe_put(map, key, value)
@spec map_get(map(), atom(), term()) :: term()
def map_get(map, key, default \\ nil) do

View File

@@ -2,28 +2,12 @@ defmodule BDS.Media.FileOps do
@moduledoc false
alias BDS.Persistence
alias BDS.ProgressReporter
alias BDS.Projects
@typedoc "An attribute map that may use atom or string keys."
@type attrs :: %{optional(atom()) => term(), optional(String.t()) => term()}
@spec attr(attrs(), atom()) :: term()
def attr(attrs, key) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> nil
end
end
@spec maybe_put(map(), atom(), term()) :: map()
def maybe_put(map, _key, nil), do: map
def maybe_put(map, key, value), do: Map.put(map, key, value)
@spec blank_to_nil(term()) :: term()
def blank_to_nil(nil), do: nil
def blank_to_nil(""), do: nil
def blank_to_nil(value), do: value
defdelegate attr(attrs, key), to: BDS.MapUtils
defdelegate maybe_put(map, key, value), to: BDS.MapUtils
defdelegate blank_to_nil(value), to: BDS.MapUtils
@spec atomic_write(Path.t(), iodata()) :: :ok | {:error, term()}
def atomic_write(path, contents), do: Persistence.atomic_write(path, contents)
@@ -95,36 +79,20 @@ defmodule BDS.Media.FileOps do
def image_mime?(mime_type), do: String.starts_with?(mime_type || "", "image/")
@spec progress_callback(keyword()) :: (float(), String.t() -> any()) | nil
def progress_callback(opts) do
case Keyword.get(opts, :on_progress) do
callback when is_function(callback, 2) -> callback
_other -> nil
end
end
def progress_callback(opts), do: ProgressReporter.callback(opts)
@spec scaled_progress_reporter((float(), String.t() -> any()) | nil, float(), float()) ::
(float(), String.t() -> any()) | nil
def scaled_progress_reporter(nil, _start_value, _end_value), do: nil
def scaled_progress_reporter(report, start_value, end_value),
do: ProgressReporter.scaled(report, start_value, end_value)
def scaled_progress_reporter(report, start_value, end_value) when is_function(report, 2) do
fn value, message ->
scaled_value = start_value + (end_value - start_value) * value
report.(scaled_value, message)
end
end
@spec report_rebuild_started((float(), String.t() -> any()) | nil, non_neg_integer(), String.t()) :: :ok
def report_rebuild_started(nil, _total, _label), do: :ok
def report_rebuild_started(callback, 0, label) do
callback.(1.0, "No #{label} found")
:ok
end
def report_rebuild_started(callback, total, label) do
callback.(0.05, "Rebuilding #{label} (0/#{total})")
:ok
end
@spec report_rebuild_started(
(float(), String.t() -> any()) | nil,
non_neg_integer(),
String.t()
) :: :ok
def report_rebuild_started(callback, total, label),
do: ProgressReporter.report_rebuild_started(callback, total, label)
@spec report_rebuild_progress(
(float(), String.t() -> any()) | nil,
@@ -132,19 +100,10 @@ defmodule BDS.Media.FileOps do
non_neg_integer(),
String.t()
) :: :ok
def report_rebuild_progress(nil, _current, _total, _label), do: :ok
def report_rebuild_progress(_callback, _current, 0, _label), do: :ok
def report_rebuild_progress(callback, current, total, label) do
callback.(0.05 + 0.95 * (current / total), "Rebuilding #{label} (#{current}/#{total})")
:ok
end
def report_rebuild_progress(callback, current, total, label),
do: ProgressReporter.report_rebuild_progress(callback, current, total, label)
@spec report_rebuild_phase((float(), String.t() -> any()) | nil, float(), String.t()) :: :ok
def report_rebuild_phase(nil, _progress, _message), do: :ok
def report_rebuild_phase(callback, progress, message) do
callback.(progress, message)
:ok
end
def report_rebuild_phase(callback, progress, message),
do: ProgressReporter.report_phase(callback, progress, message)
end

View File

@@ -2,6 +2,7 @@ defmodule BDS.Posts do
@moduledoc false
import Ecto.Query
import BDS.MapUtils, only: [attr: 2, maybe_put: 3]
alias BDS.Embeddings
alias BDS.Media
@@ -193,7 +194,8 @@ defmodule BDS.Posts do
end
end
@spec rebuild_posts_from_files(String.t(), rebuild_opts()) :: {:ok, [Post.t()]} | {:error, term()}
@spec rebuild_posts_from_files(String.t(), rebuild_opts()) ::
{:ok, [Post.t()]} | {:error, term()}
defdelegate rebuild_posts_from_files(project_id, opts \\ []), to: RebuildFromFiles
@spec discard_post_changes(String.t()) ::
@@ -211,12 +213,13 @@ defmodule BDS.Posts do
full_path = Path.join(Projects.project_data_dir(project), post.file_path)
if File.exists?(full_path) do
with {:ok, restored_post} <- RebuildFromFiles.upsert_post_from_file(post.project_id, project, full_path) do
:ok = PostLinks.sync_post_links(restored_post)
{:ok, restored_post}
else
{:error, reason} -> {:error, reason}
end
with {:ok, restored_post} <-
RebuildFromFiles.upsert_post_from_file(post.project_id, project, full_path) do
:ok = PostLinks.sync_post_links(restored_post)
{:ok, restored_post}
else
{:error, reason} -> {:error, reason}
end
else
{:error, :not_found}
end
@@ -262,12 +265,13 @@ defmodule BDS.Posts do
full_path = Path.join(Projects.project_data_dir(project), post.file_path)
if File.exists?(full_path) do
with {:ok, repaired_post} <- RebuildFromFiles.upsert_post_from_file(post.project_id, project, full_path) do
:ok = PostLinks.sync_post_links(repaired_post)
{:ok, repaired_post}
else
{:error, reason} -> {:error, reason}
end
with {:ok, repaired_post} <-
RebuildFromFiles.upsert_post_from_file(post.project_id, project, full_path) do
:ok = PostLinks.sync_post_links(repaired_post)
{:ok, repaired_post}
else
{:error, reason} -> {:error, reason}
end
else
{:error, :not_found}
end
@@ -405,7 +409,9 @@ defmodule BDS.Posts do
@spec rebuild_post_links(String.t(), rebuild_opts()) :: :ok
def rebuild_post_links(project_id, opts \\ []) do
post_ids = Repo.all(from(post in Post, where: post.project_id == ^project_id, select: post.id))
post_ids =
Repo.all(from(post in Post, where: post.project_id == ^project_id, select: post.id))
on_progress = RebuildFromFiles.progress_callback(opts)
Repo.delete_all(
@@ -414,7 +420,14 @@ defmodule BDS.Posts do
)
)
posts = Repo.all(from(post in Post, where: post.project_id == ^project_id, order_by: [asc: post.created_at]))
posts =
Repo.all(
from(post in Post,
where: post.project_id == ^project_id,
order_by: [asc: post.created_at]
)
)
total_posts = length(posts)
:ok = RebuildFromFiles.report_rebuild_started(on_progress, total_posts, "post links")
@@ -422,7 +435,9 @@ defmodule BDS.Posts do
|> Enum.with_index(1)
|> Enum.each(fn {post, index} ->
PostLinks.sync_post_links(post)
:ok = RebuildFromFiles.report_rebuild_progress(on_progress, index, total_posts, "post links")
:ok =
RebuildFromFiles.report_rebuild_progress(on_progress, index, total_posts, "post links")
end)
:ok
@@ -438,8 +453,11 @@ defmodule BDS.Posts do
@spec delete_post_translation(String.t()) :: {:ok, :deleted} | {:error, :not_found}
defdelegate delete_post_translation(translation_id), to: Translations
@spec validate_translations(String.t(), rebuild_opts()) :: {:ok, translation_validation_report()}
defdelegate validate_translations(project_id, opts \\ []), to: TranslationValidation, as: :validate
@spec validate_translations(String.t(), rebuild_opts()) ::
{:ok, translation_validation_report()}
defdelegate validate_translations(project_id, opts \\ []),
to: TranslationValidation,
as: :validate
@spec fix_invalid_translations(map()) ::
{:ok,
@@ -458,6 +476,7 @@ defmodule BDS.Posts do
project = Projects.get_project!(post.project_id)
full_path = Path.join(Projects.project_data_dir(project), post.file_path)
body = published_post_body(post, full_path)
:ok =
Persistence.atomic_write(
full_path,
@@ -544,9 +563,6 @@ defmodule BDS.Posts do
)
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp normalize_title(nil), do: ""
defp normalize_title(title), do: title
@@ -564,12 +580,4 @@ defmodule BDS.Posts do
defp has_attr?(attrs, key) do
Map.has_key?(attrs, key) or Map.has_key?(attrs, Atom.to_string(key))
end
defp attr(attrs, key) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> nil
end
end
end

View File

@@ -5,6 +5,7 @@ defmodule BDS.Posts.RebuildFromFiles do
alias BDS.Embeddings
alias BDS.Frontmatter
alias BDS.Persistence
alias BDS.ProgressReporter
alias BDS.Posts.Post
alias BDS.Posts.Slugs
alias BDS.Posts.Translation
@@ -113,7 +114,9 @@ defmodule BDS.Posts.RebuildFromFiles do
with {:ok, rebuild_file} <- parse_rebuild_file(project, full_path) do
if TranslationValidation.translation_rebuild_file?(rebuild_file) do
source_post_id = Map.fetch!(rebuild_file.fields, "translationFor")
language = TranslationValidation.normalize_language(Map.fetch!(rebuild_file.fields, "language"))
language =
TranslationValidation.normalize_language(Map.fetch!(rebuild_file.fields, "language"))
case Repo.get(Post, source_post_id) do
nil ->
@@ -133,7 +136,7 @@ defmodule BDS.Posts.RebuildFromFiles do
sync_search: true
)}
end
end
end
else
{:error, :unsupported_file}
end
@@ -267,50 +270,21 @@ defmodule BDS.Posts.RebuildFromFiles do
def parse_translation_status(status), do: String.to_existing_atom(status)
@doc false
def progress_callback(opts) do
case Keyword.get(opts, :on_progress) do
callback when is_function(callback, 2) -> callback
_other -> nil
end
end
def progress_callback(opts), do: ProgressReporter.callback(opts)
@doc false
def report_rebuild_started(nil, _total, _label), do: :ok
def report_rebuild_started(callback, 0, label) do
callback.(1.0, "No #{label} found")
:ok
end
def report_rebuild_started(callback, total, label) do
callback.(0.05, "Rebuilding #{label} (0/#{total})")
:ok
end
def report_rebuild_started(callback, total, label),
do: ProgressReporter.report_rebuild_started(callback, total, label)
@doc false
def report_rebuild_progress(nil, _current, _total, _label), do: :ok
def report_rebuild_progress(_callback, _current, 0, _label), do: :ok
def report_rebuild_progress(callback, current, total, label),
do: ProgressReporter.report_rebuild_progress(callback, current, total, label)
def report_rebuild_progress(callback, current, total, label) do
callback.(0.05 + 0.95 * (current / total), "Rebuilding #{label} (#{current}/#{total})")
:ok
end
defp scaled_progress_reporter(report, start_value, end_value),
do: ProgressReporter.scaled(report, start_value, end_value)
defp scaled_progress_reporter(nil, _start_value, _end_value), do: nil
defp scaled_progress_reporter(report, start_value, end_value) when is_function(report, 2) do
fn value, message ->
scaled_value = start_value + (end_value - start_value) * value
report.(scaled_value, message)
end
end
defp report_rebuild_phase(nil, _progress, _message), do: :ok
defp report_rebuild_phase(callback, progress, message) do
callback.(progress, message)
:ok
end
defp report_rebuild_phase(callback, progress, message),
do: ProgressReporter.report_phase(callback, progress, message)
defp unique_post_id(nil), do: Ecto.UUID.generate()

View File

@@ -2,6 +2,7 @@ defmodule BDS.Posts.Translations do
@moduledoc false
import Ecto.Query
import BDS.MapUtils, only: [attr: 2, maybe_put: 3]
alias BDS.Persistence
alias BDS.Posts
@@ -265,15 +266,4 @@ defmodule BDS.Posts.Translations do
|> String.split("-", parts: 2)
|> hd()
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp attr(attrs, key) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> nil
end
end
end

View File

@@ -0,0 +1,55 @@
defmodule BDS.ProgressReporter do
@moduledoc false
@typedoc "A 2-arity progress callback `(progress :: float(), message :: String.t()) -> any()`."
@type callback :: (float(), String.t() -> any()) | nil
@spec callback(keyword()) :: callback()
def callback(opts) do
case Keyword.get(opts, :on_progress) do
callback when is_function(callback, 2) -> callback
_other -> nil
end
end
@spec scaled(callback(), float(), float()) :: callback()
def scaled(nil, _start_value, _end_value), do: nil
def scaled(report, start_value, end_value) when is_function(report, 2) do
fn value, message ->
scaled_value = start_value + (end_value - start_value) * value
report.(scaled_value, message)
end
end
@spec report_rebuild_started(callback(), non_neg_integer(), String.t()) :: :ok
def report_rebuild_started(nil, _total, _label), do: :ok
def report_rebuild_started(callback, 0, label) do
callback.(1.0, "No #{label} found")
:ok
end
def report_rebuild_started(callback, total, label) do
callback.(0.05, "Rebuilding #{label} (0/#{total})")
:ok
end
@spec report_rebuild_progress(callback(), non_neg_integer(), non_neg_integer(), String.t()) ::
:ok
def report_rebuild_progress(nil, _current, _total, _label), do: :ok
def report_rebuild_progress(_callback, _current, 0, _label), do: :ok
def report_rebuild_progress(callback, current, total, label) do
callback.(0.05 + 0.95 * (current / total), "Rebuilding #{label} (#{current}/#{total})")
:ok
end
@spec report_phase(callback(), float(), String.t()) :: :ok
def report_phase(nil, _progress, _message), do: :ok
def report_phase(callback, progress, message) do
callback.(progress, message)
:ok
end
end

View File

@@ -3,6 +3,8 @@ defmodule BDS.Publishing do
use GenServer
import BDS.MapUtils, only: [attr: 2]
alias BDS.Persistence
alias BDS.Publishing.PublishJob
alias BDS.Projects
@@ -44,7 +46,9 @@ defmodule BDS.Publishing do
def handle_call({:update_job, job_id, attrs}, _from, state) do
reply =
case Repo.get(PublishJob, job_id) do
nil -> :ok
nil ->
:ok
job ->
attrs = Map.put(attrs, :updated_at, Persistence.now_ms())
job |> PublishJob.changeset(attrs) |> Repo.update!()
@@ -335,12 +339,4 @@ defmodule BDS.Publishing do
defp normalize_ssh_mode(mode) when mode in [:scp, :rsync], do: mode
defp normalize_ssh_mode("rsync"), do: :rsync
defp normalize_ssh_mode(_mode), do: :scp
defp attr(attrs, key) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> nil
end
end
end

View File

@@ -2,10 +2,12 @@ defmodule BDS.Scripts do
@moduledoc false
import Ecto.Query
import BDS.MapUtils, only: [attr: 2, maybe_put: 3]
alias BDS.DocumentFields
alias BDS.Frontmatter
alias BDS.Persistence
alias BDS.ProgressReporter
alias BDS.Projects
alias BDS.Repo
alias BDS.Scripts.Script
@@ -321,45 +323,15 @@ defmodule BDS.Scripts do
end
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp has_attr?(attrs, key) do
Map.has_key?(attrs, key) or Map.has_key?(attrs, Atom.to_string(key))
end
defp attr(attrs, key) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> nil
end
end
defp progress_callback(opts), do: ProgressReporter.callback(opts)
defp progress_callback(opts) do
case Keyword.get(opts, :on_progress) do
callback when is_function(callback, 2) -> callback
_other -> nil
end
end
defp report_rebuild_started(callback, total, label),
do: ProgressReporter.report_rebuild_started(callback, total, label)
defp report_rebuild_started(nil, _total, _label), do: :ok
defp report_rebuild_started(callback, 0, label) do
callback.(1.0, "No #{label} found")
:ok
end
defp report_rebuild_started(callback, total, label) do
callback.(0.05, "Rebuilding #{label} (0/#{total})")
:ok
end
defp report_rebuild_progress(nil, _current, _total, _label), do: :ok
defp report_rebuild_progress(_callback, _current, 0, _label), do: :ok
defp report_rebuild_progress(callback, current, total, label) do
callback.(0.05 + 0.95 * (current / total), "Rebuilding #{label} (#{current}/#{total})")
:ok
end
defp report_rebuild_progress(callback, current, total, label),
do: ProgressReporter.report_rebuild_progress(callback, current, total, label)
end

View File

@@ -2,11 +2,13 @@ defmodule BDS.Search do
@moduledoc false
import Ecto.Query
import BDS.MapUtils, only: [attr: 2]
alias BDS.Media.Media
alias BDS.Media.Translation, as: MediaTranslation
alias BDS.Persistence
alias BDS.Posts.Post
alias BDS.ProgressReporter
alias BDS.Projects
alias BDS.Repo
@@ -244,12 +246,7 @@ defmodule BDS.Search do
:ok
end
defp progress_callback(opts) do
case Keyword.get(opts, :on_progress) do
callback when is_function(callback, 2) -> callback
_other -> nil
end
end
defp progress_callback(opts), do: ProgressReporter.callback(opts)
defp report_reindex_started(nil, _total, _label), do: :ok
@@ -665,7 +662,9 @@ defmodule BDS.Search do
defp normalize_non_negative_integer(value, default), do: normalize_integer(value) || default
defp normalize_timestamp(nil, _position), do: nil
defp normalize_timestamp(value, _position) when is_integer(value), do: Persistence.normalize_unix_timestamp(value)
defp normalize_timestamp(value, _position) when is_integer(value),
do: Persistence.normalize_unix_timestamp(value)
defp normalize_timestamp(value, position) when is_binary(value) do
case Date.from_iso8601(value) do
@@ -680,12 +679,4 @@ defmodule BDS.Search do
end
defp blank_query?(query), do: query in [nil, ""] or String.trim(to_string(query)) == ""
defp attr(attrs, key) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> nil
end
end
end

View File

@@ -2,10 +2,12 @@ defmodule BDS.Templates do
@moduledoc false
import Ecto.Query
import BDS.MapUtils, only: [attr: 2, maybe_put: 3]
alias BDS.DocumentFields
alias BDS.Frontmatter
alias BDS.Persistence
alias BDS.ProgressReporter
alias BDS.Posts
alias BDS.Projects
alias BDS.Repo
@@ -522,45 +524,15 @@ defmodule BDS.Templates do
end
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp has_attr?(attrs, key) do
Map.has_key?(attrs, key) or Map.has_key?(attrs, Atom.to_string(key))
end
defp attr(attrs, key) do
cond do
Map.has_key?(attrs, key) -> Map.get(attrs, key)
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
true -> nil
end
end
defp progress_callback(opts), do: ProgressReporter.callback(opts)
defp progress_callback(opts) do
case Keyword.get(opts, :on_progress) do
callback when is_function(callback, 2) -> callback
_other -> nil
end
end
defp report_rebuild_started(callback, total, label),
do: ProgressReporter.report_rebuild_started(callback, total, label)
defp report_rebuild_started(nil, _total, _label), do: :ok
defp report_rebuild_started(callback, 0, label) do
callback.(1.0, "No #{label} found")
:ok
end
defp report_rebuild_started(callback, total, label) do
callback.(0.05, "Rebuilding #{label} (0/#{total})")
:ok
end
defp report_rebuild_progress(nil, _current, _total, _label), do: :ok
defp report_rebuild_progress(_callback, _current, 0, _label), do: :ok
defp report_rebuild_progress(callback, current, total, label) do
callback.(0.05 + 0.95 * (current / total), "Rebuilding #{label} (#{current}/#{total})")
:ok
end
defp report_rebuild_progress(callback, current, total, label),
do: ProgressReporter.report_rebuild_progress(callback, current, total, label)
end