aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/plugs/cache.ex
blob: a81a861d03202226061111886ee199f4100da533 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Plugs.Cache do
  @moduledoc """
  Caches successful GET responses.

  To enable the cache add the plug to a router pipeline or controller:

      plug(Pleroma.Plugs.Cache)

  ## Configuration

  To configure the plug you need to pass settings as the second argument to the `plug/2` macro:

      plug(Pleroma.Plugs.Cache, [ttl: nil, query_params: true])

  Available options:

  - `ttl`:  An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`.
  - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`.

  Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct:

      def index(conn, _params) do
        ttl = 60_000 # one minute

        conn
        |> assign(:cache_ttl, ttl)
        |> render("index.html")
      end

  """

  import Phoenix.Controller, only: [current_path: 1, json: 2]
  import Plug.Conn

  @behaviour Plug

  @defaults %{ttl: nil, query_params: true}

  @impl true
  def init([]), do: @defaults

  def init(opts) do
    opts = Map.new(opts)
    Map.merge(@defaults, opts)
  end

  @impl true
  def call(%{method: "GET"} = conn, opts) do
    key = cache_key(conn, opts)

    case Cachex.get(:web_resp_cache, key) do
      {:ok, nil} ->
        cache_resp(conn, opts)

      {:ok, record} ->
        send_cached(conn, record)

      {atom, message} when atom in [:ignore, :error] ->
        render_error(conn, message)
    end
  end

  def call(conn, _), do: conn

  # full path including query params
  defp cache_key(conn, %{query_params: true}), do: current_path(conn)

  # request path without query params
  defp cache_key(conn, %{query_params: false}), do: conn.request_path

  # request path with specific query params
  defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do
    query_string =
      conn.params
      |> Map.take(query_params)
      |> URI.encode_query()

    conn.request_path <> "?" <> query_string
  end

  defp cache_resp(conn, opts) do
    register_before_send(conn, fn
      %{status: 200, resp_body: body} = conn ->
        ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl)
        key = cache_key(conn, opts)
        content_type = content_type(conn)
        record = {content_type, body}

        Cachex.put(:web_resp_cache, key, record, ttl: ttl)

        put_resp_header(conn, "x-cache", "MISS from Pleroma")

      conn ->
        conn
    end)
  end

  defp content_type(conn) do
    conn
    |> Plug.Conn.get_resp_header("content-type")
    |> hd()
  end

  defp send_cached(conn, {content_type, body}) do
    conn
    |> put_resp_content_type(content_type, nil)
    |> put_resp_header("x-cache", "HIT from Pleroma")
    |> send_resp(:ok, body)
    |> halt()
  end

  defp render_error(conn, message) do
    conn
    |> put_status(:internal_server_error)
    |> json(%{error: message})
    |> halt()
  end
end