Current section
Files
Jump to
Current section
Files
lib/codex/files.ex
defmodule Codex.Files do
@moduledoc """
Attachment staging helpers mirroring the Python SDK file APIs.
"""
alias Codex.Files.Registry
alias Codex.Thread.Options, as: ThreadOptions
@default_ttl_ms 86_400_000
defmodule Attachment do
@moduledoc """
Represents a staged file attachment.
"""
@enforce_keys [
:id,
:name,
:path,
:checksum,
:size,
:persist,
:inserted_at,
:ttl_ms
]
defstruct [
:id,
:name,
:path,
:checksum,
:size,
:persist,
:inserted_at,
:ttl_ms
]
@type ttl :: :infinity | non_neg_integer()
@type t :: %__MODULE__{
id: String.t(),
name: String.t(),
path: Path.t(),
checksum: String.t(),
size: non_neg_integer(),
persist: boolean(),
inserted_at: DateTime.t(),
ttl_ms: ttl()
}
end
@doc """
Returns the staging directory path.
"""
@spec staging_dir() :: Path.t()
def staging_dir, do: Application.get_env(:codex_sdk, :staging_dir, default_staging_dir())
@doc """
Stages a file for future attachment invocation.
Options:
* `:name` - override the attachment name (defaults to the basename of `path`)
* `:persist` - keep file around indefinitely (default: `false`)
* `:ttl_ms` - custom time-to-live for ephemeral attachments. Accepts `:infinity` or a
non-negative integer in milliseconds. Defaults to `Application.get_env/3` lookups of
`:attachment_ttl_ms` and falls back to 24 hours.
"""
@spec stage(Path.t(), keyword()) :: {:ok, Attachment.t()} | {:error, term()}
def stage(path, opts \\ []) when is_binary(path) do
persist = Keyword.get(opts, :persist, false)
with {:ok, _pid} <- Registry.ensure_started(),
:ok <- ensure_staging_dir(),
{:ok, stat} <- File.stat(path),
{:ok, checksum} <- checksum(path),
{:ok, ttl_ms} <- resolve_ttl(opts, persist) do
name = Keyword.get(opts, :name, Path.basename(path))
stage_opts = %{
checksum: checksum,
name: name,
persist: persist,
ttl_ms: ttl_ms,
size: stat.size,
source_path: path,
destination_path: attachment_path(checksum, name)
}
Registry.stage(stage_opts)
end
end
@doc """
Lists staged attachments currently cached.
"""
@spec list_staged() :: [Attachment.t()]
def list_staged do
case list_staged_result() do
{:ok, attachments} -> attachments
{:error, _reason} -> []
end
end
@doc """
Lists staged attachments with explicit success/error tuples.
"""
@spec list_staged_result() :: {:ok, [Attachment.t()]} | {:error, term()}
def list_staged_result do
with {:ok, _pid} <- Registry.ensure_started() do
safe_registry_call(fn -> {:ok, Registry.list()} end)
end
end
@doc """
Removes staged files that are not marked as persistent.
"""
@spec force_cleanup() :: :ok | {:error, term()}
def force_cleanup do
with {:ok, _pid} <- Registry.ensure_started() do
safe_registry_call(fn -> Registry.force_cleanup() end)
end
end
@doc """
Deprecated alias retained for backwards compatibility.
"""
@spec cleanup!() :: :ok | {:error, term()}
def cleanup!, do: force_cleanup()
@doc """
Resets the staging directory and manifest.
"""
@spec reset!() :: :ok | {:error, term()}
def reset! do
with {:ok, _pid} <- Registry.ensure_started() do
safe_registry_call(fn -> Registry.reset(staging_dir()) end)
end
end
@doc """
Returns high-level staging metrics including counts and total bytes.
"""
@spec metrics() :: map() | {:error, term()}
def metrics do
with {:ok, _pid} <- Registry.ensure_started() do
safe_registry_call(fn -> Registry.metrics() end)
end
end
@doc """
Attaches an `Attachment` to the supplied `ThreadOptions`.
"""
@spec attach(ThreadOptions.t(), Attachment.t()) :: ThreadOptions.t()
def attach(%ThreadOptions{} = opts, %Attachment{} = attachment) do
updated =
opts.attachments
|> Enum.reject(&(&1.id == attachment.id))
|> Kernel.++([attachment])
%{opts | attachments: updated}
end
defp checksum(path) do
with {:ok, data} <- File.read(path) do
{:ok, :crypto.hash(:sha256, data) |> Base.encode16(case: :lower)}
end
end
defp resolve_ttl(_opts, true), do: {:ok, :infinity}
defp resolve_ttl(opts, false) do
case Keyword.get(opts, :ttl_ms, default_ttl()) do
:infinity ->
{:ok, :infinity}
ttl when is_integer(ttl) and ttl >= 0 ->
{:ok, ttl}
invalid ->
{:error, {:invalid_ttl, invalid}}
end
end
defp default_ttl do
Application.get_env(:codex_sdk, :attachment_ttl_ms, @default_ttl_ms)
end
defp default_staging_dir do
Path.join(System.tmp_dir!(), "codex_files")
end
defp ensure_staging_dir do
File.mkdir_p(staging_dir())
end
defp attachment_path(checksum, name) do
Path.join([staging_dir(), checksum, name])
end
defp safe_registry_call(fun) when is_function(fun, 0) do
fun.()
catch
:exit, reason -> {:error, reason}
end
end