defmodule GitGudWeb.Api.Mobile.PackageController do
@moduledoc """
The repository's published packages, and how to install them.
Read-only, and deliberately so. Publishing is something a CI job does
with a token scoped to one repository; a phone has no build to
publish and no business holding a credential that could.
Three endpoints, shaped by what the screen actually needs:
* `index` — every format with something published, each with its
packages. One call fills the tab, because a list that needs a
request per format is a list that renders in stages.
* `show` — one package's versions, with the files under each and
the one-liner that installs it.
* `usage` — the full setup instructions for one format, fetched
when the reader opens them rather than attached to every list.
## Why the instructions come from the server
`GitGud.Packages.Usage` already renders them for the web page, and
they are full of things only the server knows: the registry's own
URL, the namespace a repository belongs to, whether a Hex repo is
named after the org or the repository. A client that rebuilt those
strings would be wrong the first time a deployment moved.
"""
use GitGudWeb, :controller
import GitGudWeb.Api.Mobile.Helpers
alias GitGud.Packages
alias GitGud.Packages.Format
alias GitGud.Packages.Usage
alias GitGudWeb.Api.Mobile.Auth
alias GitGudWeb.Api.Mobile.JSON
# Containers first — the oldest surface here — then the language
# registries in the order they landed. Same order the web page uses,
# so someone looking at both sees the same thing twice.
@format_order ~w(oci pypi hex cargo deb rpm fdroid)
def index(conn, %{"owner" => owner, "name" => name}) do
with {:ok, user} <- Auth.require_scope(conn, "packages:read"),
{:ok, repo} <- fetch_repo(conn, user, owner, name) do
ctx = usage_context(repo)
formats =
repo
|> Packages.list_packages()
|> Enum.group_by(& &1.format)
|> Enum.sort_by(fn {format, _} -> order_of(format) end)
|> Enum.map(fn {format, packages} ->
JSON.package_format(format, packages, ctx)
end)
# Every format this forge can serve, not just the ones with
# something in them: an empty tab's only useful content is "here
# is how you would publish", and the client can't name the
# formats itself without carrying a list that goes stale.
json(conn, %{"formats" => formats, "available_formats" => Format.formats()})
else
{:error, conn} -> conn
end
end
def show(conn, %{"owner" => owner, "name" => name, "format" => format, "package" => package}) do
with {:ok, user} <- Auth.require_scope(conn, "packages:read"),
{:ok, repo} <- fetch_repo(conn, user, owner, name),
{:ok, package} <- fetch_package(conn, repo, format, package) do
json(conn, JSON.package_detail(package, usage_context(repo)))
else
{:error, conn} -> conn
end
end
@doc """
A package's icon, as image bytes — what a list or detail's
`icon_url` points at. Here rather than on the web route so it
authenticates like the rest of this API, with the bearer token.
"""
def icon(conn, %{"owner" => owner, "name" => name, "format" => format, "package" => package}) do
with {:ok, user} <- Auth.require_scope(conn, "packages:read"),
{:ok, repo} <- fetch_repo(conn, user, owner, name),
{:ok, package} <- fetch_package(conn, repo, format, package) do
case Packages.icon(package) do
{:ok, content_type, bytes, sha256} ->
conn
|> put_resp_header("etag", ~s("#{sha256}"))
|> put_resp_header("cache-control", "private, max-age=300")
|> put_resp_content_type(content_type, nil)
|> send_resp(200, bytes)
:none ->
send_error(conn, 404, "this package has no icon")
end
else
{:error, conn} -> conn
end
end
@doc """
Setup instructions for one format under this repository.
Served even when the format has nothing published yet: "here is how
you would publish to it" is the useful answer to an empty tab, and
the client has no way to render it itself.
"""
def usage(conn, %{"owner" => owner, "name" => name, "format" => format}) do
with {:ok, user} <- Auth.require_scope(conn, "packages:read"),
{:ok, repo} <- fetch_repo(conn, user, owner, name),
{:ok, _mod} <- known_format(conn, format) do
json(conn, JSON.package_usage(format, usage_context(repo)))
else
{:error, conn} -> conn
end
end
defp usage_context(repo),
do: Usage.context(repo, Application.get_env(:git_gud, :registry_host))
defp fetch_package(conn, repo, format, name) do
case Packages.get_package(repo, format, name) do
nil -> {:error, send_error(conn, 404, "no #{format} package '#{name}' in this repository")}
package -> {:ok, package}
end
end
# `oci` has no adapter — zot serves it — but it is a format this
# forge publishes and has instructions for, so it is known here even
# though `Format.adapter/1` doesn't answer for it.
defp known_format(conn, format) do
if format in Format.formats() do
{:ok, format}
else
{:error, send_error(conn, 404, "unknown package format '#{format}'")}
end
end
defp order_of(format) do
case Enum.find_index(@format_order, &(&1 == format)) do
nil -> length(@format_order)
index -> index
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
5.4 KiB · text
History
b868907