NervesGithubUpdater.GithubClient (nerves_github_updater v0.1.0)

Copy Markdown View Source

Stateless HTTP client for GitHub Releases.

Library-shape: no host dependencies. The progress callback is the only seam; consumers translate it into PubSub broadcasts.

Rate limiting

Anonymous requests are limited to 60/hr per source IP. Pass an If-None-Match ETag (returned in the previous latest_release/2 response under :etag) — 304 responses do not count against quota.

See: https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api

Summary

Functions

Streams an asset to dest_path, writing atomically (.part → rename).

Fetches the latest release for owner_repo (e.g. "bbangert/universal_proxy").

Types

asset()

@type asset() :: %{
  :name => String.t(),
  :url => String.t(),
  :size => non_neg_integer(),
  optional(any()) => any()
}

release()

@type release() :: %{
  tag_name: String.t(),
  name: String.t() | nil,
  body: String.t(),
  published_at: String.t(),
  assets: [asset()],
  etag: String.t() | nil
}

Functions

download_asset(asset_url, dest_path, opts \\ [])

@spec download_asset(String.t(), Path.t(), keyword()) :: :ok | {:error, term()}

Streams an asset to dest_path, writing atomically (.part → rename).

Reports progress through opts[:progress] as {:downloading, percent} every ≥ 262144 bytes and once on completion.

Options

  • :expected_sha256 — optional hex digest (either case) checked against a SHA-256 computed incrementally while streaming. Verified before the .part → dest rename; on mismatch the .part file is deleted and {:error, {:sha256_mismatch, expected: ..., actual: ...}} is returned (both hex strings lowercase). Omit to skip hashing entirely.

A hard size ceiling always applies — max(expected_size * 2, 268435456) bytes — so a runaway or malicious response can't fill the download dir even when :expected_size is 0/absent. Exceeding it aborts the stream and returns {:error, {:download_too_large, limit}}.

Returns :ok on success; on failure deletes the partial file and returns {:error, term()}.

latest_release(owner_repo, opts \\ [])

@spec latest_release(
  String.t(),
  keyword()
) :: {:ok, release()} | {:ok, :not_modified} | {:error, term()}

Fetches the latest release for owner_repo (e.g. "bbangert/universal_proxy").

Options

  • :channel:stable (default) or :prerelease. :stable hits the /releases/latest endpoint — GitHub's "latest" excludes prereleases and drafts by definition, so the existing prerelease guard is just belt-and-suspenders. :prerelease hits /releases?per_page=30 and picks the release with the highest semver tag (drafts excluded, prereleases eligible — ties are broken by Version.compare/2's native prerelease ordering, so e.g. v1.3.0-rc.1 beats v1.2.9).
  • :github_token — optional bearer token for higher rate limits and private-repo access.
  • :etag — value from a prior call's response. A matching server response returns {:ok, :not_modified} without counting against quota.
  • :req_options — extra options forwarded to Req.new/1 (used by tests to inject plug: for stubbing).

Returns {:ok, release} on a current release, {:ok, :not_modified} on 304, {:error, :no_release} when :channel is :prerelease and no release has a parseable semver tag, {:error, :rate_limited} on quota exhaustion, {:error, :not_found} on 404, or {:error, term()} on transport errors.