a81a861d03202226061111886ee199f4100da533
[akkoma] / lib / pleroma / plugs / cache.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Plugs.Cache do
6 @moduledoc """
7 Caches successful GET responses.
8
9 To enable the cache add the plug to a router pipeline or controller:
10
11 plug(Pleroma.Plugs.Cache)
12
13 ## Configuration
14
15 To configure the plug you need to pass settings as the second argument to the `plug/2` macro:
16
17 plug(Pleroma.Plugs.Cache, [ttl: nil, query_params: true])
18
19 Available options:
20
21 - `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`.
22 - `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`.
23
24 Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct:
25
26 def index(conn, _params) do
27 ttl = 60_000 # one minute
28
29 conn
30 |> assign(:cache_ttl, ttl)
31 |> render("index.html")
32 end
33
34 """
35
36 import Phoenix.Controller, only: [current_path: 1, json: 2]
37 import Plug.Conn
38
39 @behaviour Plug
40
41 @defaults %{ttl: nil, query_params: true}
42
43 @impl true
44 def init([]), do: @defaults
45
46 def init(opts) do
47 opts = Map.new(opts)
48 Map.merge(@defaults, opts)
49 end
50
51 @impl true
52 def call(%{method: "GET"} = conn, opts) do
53 key = cache_key(conn, opts)
54
55 case Cachex.get(:web_resp_cache, key) do
56 {:ok, nil} ->
57 cache_resp(conn, opts)
58
59 {:ok, record} ->
60 send_cached(conn, record)
61
62 {atom, message} when atom in [:ignore, :error] ->
63 render_error(conn, message)
64 end
65 end
66
67 def call(conn, _), do: conn
68
69 # full path including query params
70 defp cache_key(conn, %{query_params: true}), do: current_path(conn)
71
72 # request path without query params
73 defp cache_key(conn, %{query_params: false}), do: conn.request_path
74
75 # request path with specific query params
76 defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do
77 query_string =
78 conn.params
79 |> Map.take(query_params)
80 |> URI.encode_query()
81
82 conn.request_path <> "?" <> query_string
83 end
84
85 defp cache_resp(conn, opts) do
86 register_before_send(conn, fn
87 %{status: 200, resp_body: body} = conn ->
88 ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl)
89 key = cache_key(conn, opts)
90 content_type = content_type(conn)
91 record = {content_type, body}
92
93 Cachex.put(:web_resp_cache, key, record, ttl: ttl)
94
95 put_resp_header(conn, "x-cache", "MISS from Pleroma")
96
97 conn ->
98 conn
99 end)
100 end
101
102 defp content_type(conn) do
103 conn
104 |> Plug.Conn.get_resp_header("content-type")
105 |> hd()
106 end
107
108 defp send_cached(conn, {content_type, body}) do
109 conn
110 |> put_resp_content_type(content_type, nil)
111 |> put_resp_header("x-cache", "HIT from Pleroma")
112 |> send_resp(:ok, body)
113 |> halt()
114 end
115
116 defp render_error(conn, message) do
117 conn
118 |> put_status(:internal_server_error)
119 |> json(%{error: message})
120 |> halt()
121 end
122 end