Packages
phoenix_kit
1.7.206
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/sitemap/file_storage.ex
defmodule PhoenixKit.Modules.Sitemap.FileStorage do
@moduledoc """
File-based storage for sitemap XML files.
Supports both a single index file and per-module sitemap files:
priv/static/sitemap.xml -> sitemapindex (index)
priv/static/sitemaps/sitemap-static.xml -> static pages
priv/static/sitemaps/sitemap-routes.xml -> router discovery
priv/static/sitemaps/sitemap-publishing.xml -> publishing posts
priv/static/sitemaps/sitemap-shop.xml -> shop products
priv/static/sitemaps/sitemap-entities.xml -> entity records
## Key Features
- **Direct nginx serving** - Files in priv/static/ can be served without Phoenix
- **ETag from mtime** - Use `get_file_stat/0` for cache validation
- **On-demand generation** - First request generates if file missing
- **Per-module files** - Independent generation and caching per source
"""
require Logger
@sitemap_file "sitemap.xml"
@sitemaps_subdir "sitemaps"
# ── Index file operations (sitemap.xml) ────────────────────────────
@doc """
Saves XML content to the index sitemap file.
"""
@spec save_index(String.t()) :: :ok | {:error, term()}
def save_index(xml_content), do: save(xml_content)
@doc """
Loads XML content from the index sitemap file.
"""
@spec load_index() :: {:ok, String.t()} | :error
def load_index, do: load()
@doc """
Checks if the index sitemap file exists.
"""
@spec index_exists?() :: boolean()
def index_exists?, do: exists?()
# ── Per-module file operations (sitemaps/*.xml) ────────────────────
@doc """
Saves XML content for a specific module sitemap file.
Filename should NOT include the `.xml` extension.
## Examples
FileStorage.save_module("sitemap-shop", xml_content)
# Saves to priv/static/sitemaps/sitemap-shop.xml
"""
@spec save_module(String.t(), String.t()) :: :ok | {:error, term()}
def save_module(filename, xml_content)
when is_binary(filename) and is_binary(xml_content) do
path = module_file_path(filename)
with :ok <- ensure_directory_exists(path),
:ok <- File.write(path, xml_content) do
Logger.debug("FileStorage: Saved #{filename}.xml (#{byte_size(xml_content)} bytes)")
:ok
else
{:error, reason} = error ->
Logger.warning("FileStorage: Failed to save #{filename}.xml: #{inspect(reason)}")
error
end
end
@doc """
Loads XML content for a specific module sitemap file.
"""
@spec load_module(String.t()) :: {:ok, String.t()} | :error
def load_module(filename) when is_binary(filename) do
path = module_file_path(filename)
case File.read(path) do
{:ok, content} ->
Logger.debug("FileStorage: Loaded #{filename}.xml from file")
{:ok, content}
{:error, :enoent} ->
:error
{:error, reason} ->
Logger.warning("FileStorage: Failed to read #{filename}.xml: #{inspect(reason)}")
:error
end
end
@doc """
Checks if a specific module sitemap file exists.
"""
@spec module_exists?(String.t()) :: boolean()
def module_exists?(filename) when is_binary(filename) do
filename |> module_file_path() |> File.exists?()
end
@doc """
Deletes a specific module sitemap file.
"""
@spec delete_module(String.t()) :: :ok
def delete_module(filename) when is_binary(filename) do
path = module_file_path(filename)
case File.rm(path) do
:ok ->
Logger.debug("FileStorage: Deleted #{filename}.xml")
:ok
{:error, :enoent} ->
:ok
{:error, reason} ->
Logger.warning("FileStorage: Failed to delete #{filename}.xml: #{inspect(reason)}")
:ok
end
end
@doc """
Returns file stats for a specific module sitemap file.
"""
@spec get_module_stat(String.t()) :: {:ok, tuple(), non_neg_integer()} | :error
def get_module_stat(filename) when is_binary(filename) do
case File.stat(module_file_path(filename)) do
{:ok, %{mtime: mtime, size: size}} -> {:ok, mtime, size}
{:error, _} -> :error
end
end
@doc """
Lists all `.xml` files in the sitemaps subdirectory.
Returns list of filenames without the `.xml` extension.
"""
@spec list_module_files() :: [String.t()]
def list_module_files do
dir = sitemaps_dir()
if File.dir?(dir) do
dir
|> File.ls!()
|> Enum.filter(&String.ends_with?(&1, ".xml"))
|> Enum.map(&String.trim_trailing(&1, ".xml"))
|> Enum.sort()
else
[]
end
rescue
_ -> []
end
@doc """
Deletes all module sitemap files in the sitemaps subdirectory.
"""
@spec delete_all_modules() :: :ok
def delete_all_modules do
dir = sitemaps_dir()
if File.dir?(dir) do
dir
|> File.ls!()
|> Enum.filter(&String.ends_with?(&1, ".xml"))
|> Enum.each(fn file ->
File.rm(Path.join(dir, file))
end)
end
:ok
rescue
_ -> :ok
end
@doc """
Returns the path to the sitemaps subdirectory.
"""
@spec sitemaps_dir() :: String.t()
def sitemaps_dir do
Path.join(storage_dir(), @sitemaps_subdir)
end
@doc """
Returns the full path for a module sitemap file.
"""
@spec module_file_path(String.t()) :: String.t()
def module_file_path(filename) when is_binary(filename) do
Path.join(sitemaps_dir(), "#{filename}.xml")
end
# ── Legacy / backward-compatible API ───────────────────────────────
@doc """
Saves XML content to the sitemap file.
Creates the storage directory if it doesn't exist.
"""
@spec save(String.t()) :: :ok | {:error, term()}
def save(xml_content) when is_binary(xml_content) do
path = file_path()
with :ok <- ensure_directory_exists(path),
:ok <- File.write(path, xml_content) do
Logger.debug("FileStorage: Saved sitemap.xml (#{byte_size(xml_content)} bytes)")
:ok
else
{:error, reason} = error ->
Logger.warning("FileStorage: Failed to save sitemap.xml: #{inspect(reason)}")
error
end
end
@doc """
Loads XML content from the sitemap file.
"""
@spec load() :: {:ok, String.t()} | :error
def load do
path = file_path()
case File.read(path) do
{:ok, content} ->
Logger.debug("FileStorage: Loaded sitemap.xml from file")
{:ok, content}
{:error, :enoent} ->
:error
{:error, reason} ->
Logger.warning("FileStorage: Failed to read sitemap.xml: #{inspect(reason)}")
:error
end
end
@doc """
Checks if the sitemap file exists.
"""
@spec exists?() :: boolean()
def exists? do
file_path() |> File.exists?()
end
@doc """
Returns file stats for ETag generation.
"""
@spec get_file_stat() :: {:ok, tuple(), non_neg_integer()} | :error
def get_file_stat do
case File.stat(file_path()) do
{:ok, %{mtime: mtime, size: size}} -> {:ok, mtime, size}
{:error, _} -> :error
end
end
@doc """
Deletes the sitemap file to force regeneration.
"""
@spec delete() :: :ok
def delete do
case File.rm(file_path()) do
:ok ->
Logger.debug("FileStorage: Deleted sitemap.xml")
:ok
{:error, :enoent} ->
:ok
{:error, reason} ->
Logger.warning("FileStorage: Failed to delete sitemap.xml: #{inspect(reason)}")
:ok
end
end
@doc """
Returns the file path for the sitemap.
"""
@spec file_path() :: String.t()
def file_path do
Path.join(storage_dir(), @sitemap_file)
end
@doc """
Returns the storage directory path.
"""
@spec storage_dir() :: String.t()
def storage_dir do
case :code.priv_dir(:phoenix_kit) do
{:error, :bad_name} -> "priv/static"
priv_dir -> Path.join(priv_dir, "static")
end
end
@doc """
Clears the sitemap file. Alias for `delete/0`.
"""
@spec clear_all() :: :ok
def clear_all do
delete()
delete_all_modules()
end
# Legacy API compatibility - style parameter ignored
@doc false
@spec save(String.t(), String.t()) :: :ok | {:error, term()}
def save(_style, xml_content), do: save(xml_content)
@doc false
@spec load(String.t()) :: {:ok, String.t()} | :error
def load(_style), do: load()
@doc false
@spec exists?(String.t()) :: boolean()
def exists?(_style), do: exists?()
@doc false
@spec file_path(String.t()) :: String.t()
def file_path(_style), do: file_path()
# Private helpers
defp ensure_directory_exists(file_path) do
dir = Path.dirname(file_path)
case File.mkdir_p(dir) do
:ok -> :ok
{:error, reason} -> {:error, {:mkdir_failed, reason}}
end
end
end