Current section
Files
Jump to
Current section
Files
lib/time_zone_info/updater/impl.ex
defmodule TimeZoneInfo.Updater.Impl do
@moduledoc false
# Handles the automatic update and the initial setup.
@behaviour TimeZoneInfo.Updater
alias TimeZoneInfo.DataConfig
alias TimeZoneInfo.DataPersistence
alias TimeZoneInfo.DataStore
alias TimeZoneInfo.Downloader
alias TimeZoneInfo.ExternalTermFormat
alias TimeZoneInfo.FileArchive
alias TimeZoneInfo.IanaParser
alias TimeZoneInfo.Listener
alias TimeZoneInfo.Transformer
alias TimeZoneInfo.UtcDateTime
require Logger
@type step ::
:run
| :initial
| :force
| :check
| :disabled
| :finally
| :maybe
@seconds_per_day 24 * 60 * 60
@default_lookahead 5
@doc """
Updates the TimeZoneInfo data.
With `opt` `:run` an normal update process is started. The `opt` `:force`
forced an update process.
Returns
- `:ok` if automatic updates are disabled.
- `{:next, seconds}` where `seconds` is the time to wait until the next update.
- `{:error, reason}` in case of an error.
"""
@spec update(opt :: :run | :force) :: :ok | {:next, Calendar.second()} | {:error, term()}
def update(step \\ :run) when step in [:run, :force] do
with {:error, _} = error <- step |> step() |> do_update() do
Listener.on_update(error)
error
end
end
defp step(:run) do
case DataStore.empty?() do
true -> :initial
false -> :check
end
end
defp step(step), do: step
defp do_update(:initial) do
Listener.on_update(:initial)
with {:ok, data} <- DataPersistence.fetch(),
{:ok, data_config} <- fetch_data_config(),
{:ok, data} <- DataConfig.update_time_zones(data, data_config[:time_zones]) do
do_update({:initial, data, DataConfig.equal?(data, data_config)})
else
{:error, {:time_zones_not_found, _}} = error ->
force_update(error)
{:error, :enoent} = error ->
force_update(error)
error ->
error
end
end
defp do_update({:initial, data, true}) do
with :ok <- DataStore.put(data) do
do_update(:check)
end
end
defp do_update({:initial, data, false}) do
Listener.on_update(:config_changed)
with :disabled <- force_update(:disabled) do
DataStore.put(data)
end
end
defp do_update(:force), do: force_update(:ok)
defp do_update(:check) do
Listener.on_update(:check)
with {:ok, interval} <- fetch_env(:update), do: do_update(interval)
end
defp do_update(:disabled), do: :ok
defp do_update(:daily) do
with {:ok, last_update} <- DataPersistence.fetch_last_update() do
now = UtcDateTime.now(:unix)
case last_update + @seconds_per_day - now do
next when next > 0 ->
Listener.on_update(:not_required)
{:next, now + next}
_next ->
with :ok <- do_update(:maybe),
:ok <- DataPersistence.put_last_update(now) do
{:next, now + @seconds_per_day}
end
end
end
end
defp do_update(:maybe) do
with {:ok, checksum_persistence} <- DataPersistence.checksum(),
{:ok, data} when not is_atom(data) <- download(),
{:ok, checksum_download} <- ExternalTermFormat.checksum(data) do
case checksum_persistence == checksum_download do
true ->
Listener.on_update(:up_to_date)
:ok
false ->
do_update(:finally, data)
end
else
{:ok, :not_modified} ->
Listener.on_update(:up_to_date)
:ok
error ->
error
end
end
defp do_update(:finally, data) do
Listener.on_update(:update)
with {:ok, time_zones} <- fetch_env(:time_zones),
{:ok, data} <- DataConfig.update_time_zones(data, time_zones),
:ok <- DataStore.put(data) do
DataPersistence.put(data)
end
end
defp force_update(on_disabled) do
Listener.on_update(:force)
now = UtcDateTime.now(:unix)
with {:ok, update} when update != :disabled <- fetch_env(:update),
{:ok, data} when not is_atom(data) <- download(),
:ok <- do_update(:finally, data) do
{:next, now + @seconds_per_day}
else
{:ok, :not_modified} ->
up_to_date(now)
{:ok, :disabled} ->
Listener.on_update(:disabled)
on_disabled
error ->
error
end
end
defp up_to_date(timestamp) do
with :ok <- DataPersistence.put_last_update(timestamp) do
Listener.on_update(:up_to_date)
{:next, timestamp + @seconds_per_day}
end
end
defp download do
Listener.on_update(:download)
with {:ok, files} <- files(),
{:ok, time_zones} <- fetch_env(:time_zones),
{:ok, lookahead} <- fetch_env(:lookahead) do
opts = [files: files, time_zones: time_zones, lookahead: lookahead]
opts =
case DataPersistence.checksum() do
{:ok, checksum} -> Keyword.put(opts, :checksum, checksum)
_error -> opts
end
case Downloader.download(opts) do
{:ok, :iana, {200, data}} ->
transform(data, opts)
{:ok, mode, {200, data}} when mode in [:etf, :ws] ->
ExternalTermFormat.decode(data)
{:ok, _mode, {304, _body}} ->
{:ok, :not_modified}
{:ok, _mode, response} ->
{:error, response}
error ->
error
end
end
end
defp transform(data, opts) do
with {:ok, version, content} <- extract(data),
{:ok, parsed} <- IanaParser.parse(content) do
{:ok, Transformer.transform(parsed, version, opts)}
end
end
defp extract(data) do
with {:ok, files} <- files(),
{:ok, contents} <- FileArchive.extract(data, files) do
{version, contents} = Map.pop(contents, "version")
{:ok, String.trim(version), join(contents)}
end
end
defp join(files) do
Enum.map_join(files, "\n", fn {_name, content} -> content end)
end
defp files do
with {:ok, files} <- fetch_env(:files) do
{:ok, ["version" | files]}
end
end
defp fetch_data_config do
with {:ok, time_zones} <- fetch_env(:time_zones),
{:ok, lookahead} <- fetch_env(:lookahead),
{:ok, files} <- fetch_env(:files) do
{:ok, [time_zones: time_zones, lookahead: lookahead, files: files]}
end
end
defp fetch_env(:files) do
with :error <- Application.fetch_env(:time_zone_info, :files) do
{:error, {:invalid_config, :files}}
end
end
defp fetch_env(:update) do
case Application.fetch_env(:time_zone_info, :update) do
{:ok, value} when value in [:disabled, :daily] ->
{:ok, value}
{:ok, value} ->
{:error, {:invalid_config, [update: value]}}
:error ->
{:error, {:invalid_config, :update}}
end
end
defp fetch_env(:lookahead) do
case Application.get_env(:time_zone_info, :lookahead, @default_lookahead) do
value when value > 0 ->
{:ok, value}
value ->
{:error, {:invalid_config, [lookahead: value]}}
end
end
defp fetch_env(:time_zones) do
case Application.get_env(:time_zone_info, :time_zones, :all) do
:all ->
{:ok, :all}
[] ->
{:error, {:invalid_config, [time_zones: []]}}
[_ | _] = list ->
validate_time_zones(list)
value ->
{:error, {:invalid_config, [time_zones: value]}}
end
end
defp validate_time_zones(list) do
case Enum.all?(list, fn item -> is_binary(item) end) do
true -> {:ok, list}
false -> {:error, {:invalid_config, [time_zones: list]}}
end
end
end