# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved. use Croma defmodule AntikytheraCore.Path do alias Antikythera.{Env, GearName, TenantId, SecondsSinceEpoch} @temp_file_suffix "-temp" # # paths under antikythera root directory # if Env.compiling_for_cloud?() do # Use compile-time application config @antikythera_root_dir Application.compile_env!(:antikythera, :antikythera_root_dir) defun antikythera_root_dir() :: Path.t(), do: @antikythera_root_dir defun compiled_core_dir() :: Path.t(), do: Path.join(antikythera_root_dir(), "releases") else # Use runtime application config defun antikythera_root_dir() :: Path.t(), do: Application.fetch_env!(:antikythera, :antikythera_root_dir) defun compiled_core_dir() :: Path.t() do parent_dir = Path.join([__DIR__, "..", "..", ".."]) |> Path.expand() release_generating_project_dir = case Path.basename(parent_dir) do # from antikythera instance or gear projects "deps" -> Path.join([parent_dir, ".."]) # as a standalone project _ -> Path.join([__DIR__, "..", ".."]) end |> Path.expand() Path.join([ release_generating_project_dir, "rel_local_erlang-#{System.otp_release()}", "#{Env.antikythera_instance_name()}", "releases" ]) end end defun core_config_file_path() :: Path.t(), do: Path.join([antikythera_root_dir(), "config", "antikythera"]) defun gear_config_dir() :: Path.t(), do: Path.join(antikythera_root_dir(), "gear_config") defun history_dir() :: Path.t(), do: Path.join(antikythera_root_dir(), "history") defun compiled_gears_dir() :: Path.t(), do: Path.join(antikythera_root_dir(), "compiled_gears_erlang-#{System.otp_release()}") defunp tenant_dir() :: Path.t(), do: Path.join(antikythera_root_dir(), "tenant") defun tenant_ids_file_path() :: Path.t(), do: Path.join(tenant_dir(), "ids.gz") defun tenant_setting_dir() :: Path.t(), do: Path.join(tenant_dir(), "setting") defun tenant_setting_file_path(id :: v[TenantId.t()]) :: Path.t(), do: Path.join(tenant_setting_dir(), id) defun gear_config_file_path(gear_name :: v[GearName.t()]) :: Path.t() do Path.join(gear_config_dir(), Atom.to_string(gear_name)) end # # paths under unpacked release directory # defun gear_log_dir(gear_name :: v[GearName.t()]) :: Path.t() do Path.join([Application.app_dir(:antikythera), "..", "..", "log", Atom.to_string(gear_name)]) |> Path.expand() end defun gear_log_file_path(gear_name :: v[GearName.t()]) :: Path.t() do Path.join(gear_log_dir(gear_name), "#{gear_name}.log.gz") end defun core_log_file_path(name :: v[String.t()]) :: Path.t() do Path.join(gear_log_dir(:antikythera), "#{name}.log.gz") end # # paths under "secret" directory in each node # defunp secret_dir() :: Path.t() do if Env.running_in_cloud?() do # At runtime `:code.root_dir/0` returns the unpacked "release" directory; "secret" is placed next to "release" Path.join([:code.root_dir(), "..", "secret"]) |> Path.expand() else case System.get_env("ANTIKYTHERA_SECRET_DIR") do nil -> Path.join([antikythera_root_dir(), "..", "secret"]) |> Path.expand() dir -> dir end end end defun config_encryption_key_path() :: Path.t() do Path.join(secret_dir(), "config_encryption_key") end defun system_info_access_token_path() :: Path.t() do Path.join(secret_dir(), "system_info_access_token") end # # path for Antikythera.Tmpdir # defun gear_tmp_dir() :: Path.t() do # This must be fetched at runtime at least in non-cloud environment in order to use # path with the current OS pid, not with the OS pid of `$ mix compile` command. Application.fetch_env!(:antikythera, :gear_tmp_dir) end # # path for RaftedValue & RaftFleet # defun raft_persistence_dir_parent() :: Path.t() do # This must be fetched at runtime at least in non-cloud environment in order to use # path with the current OS pid, not with the OS pid of `$ mix compile` command. Application.fetch_env!(:antikythera, :raft_persistence_dir_parent) end # # utilities # # In order not to miss file modifications due to NFS caching (if any) and/or clock skew, # we make margin by shifting `since` by a few seconds. # We don't care if the same modification is observed multiple times here. # The value comes from: default max lifetime of NFS client-side caches (60s) added by 2s to avoid issues due to clock skew. # (Should we make this a mix config item?) @file_modification_time_margin_in_seconds if Env.compiling_for_cloud?(), do: 62, else: 0 defun changed?(path :: Path.t(), since :: v[SecondsSinceEpoch.t()]) :: boolean do since_with_margin = max(0, since - @file_modification_time_margin_in_seconds) %File.Stat{mtime: mtime} = File.stat!(path, time: :posix) since_with_margin <= mtime end defun list_modified_files(dir :: Path.t(), since :: v[SecondsSinceEpoch.t()]) :: [String.t()] do since_with_margin = max(0, since - @file_modification_time_margin_in_seconds) Path.wildcard(Path.join(dir, "*")) |> Enum.filter(fn path -> modified_regular_file?(path, since_with_margin) and not String.ends_with?(path, @temp_file_suffix) end) |> Enum.sort() end defunp modified_regular_file?(path :: Path.t(), since :: v[SecondsSinceEpoch.t()]) :: boolean do case File.stat(path, time: :posix) do {:ok, %File.Stat{type: :regular, mtime: t}} when t >= since -> true {:ok, _unchanged_or_not_regular} -> false # removed between `Path.wildcard/1` and `File.stat/2` {:error, :enoent} -> false end end defun atomic_write!( path :: Path.t(), content :: iodata() ) :: :ok do node_name = Node.self() |> Atom.to_string() # Convert `#PID<0.23069.636>` to `0.23069.636` pid_path_safe_string = inspect(self()) |> String.split("<") |> Enum.at(1) |> String.replace(">", "") temp_path = "#{path}-#{node_name}-#{pid_path_safe_string}#{@temp_file_suffix}" File.write!(temp_path, content, [:sync]) File.rename!(temp_path, path) end end