-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathhits.ex
175 lines (136 loc) · 4.74 KB
/
hits.ex
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
defmodule App.Hits do
@moduledoc """
App.Hits contains all hits-related helper functions.
"""
require Hash
@doc """
svg_badge_template/0 opens the SVG template file.
the function is single-purpose so that we can _cache_ the template.
see: https://github.com/dwyl/hits-elixir/issues/3 #helpwanted
returns String of template.
"""
# help wanted caching this!
def svg_badge_template do
# see: github.com/dwyl/hits-elixir/issues/3
File.read!("./lib/template.svg")
end
@doc """
make_badge/1 from svg template substituting the count value
## Parameters
- count: Number the view/hit count to be displayed in the badge.
Returns the badge XML with the count.
"""
def make_badge(count \\ 1) do
String.replace(svg_badge_template(), ~r/{count}/, to_string(count))
end
@doc """
get_user_agent_string/1 extracts user-agent, IP address and browser language
from the Plug.Conn map see: https://hexdocs.pm/plug/Plug.Conn.html
> there is probably a *much* better way of doing this ... PR v. welcome!
## Parameters
- conn: Map the standard Plug.Conn info see: hexdocs.pm/plug/Plug.Conn.html
Returns String with user-agent, IP and language separated by "pipe" charater.
"""
def get_user_agent_string(conn) do
[{_, ua}] = Enum.filter(conn.req_headers, fn {k, _} ->
k == "user-agent" end)
[{_, langs}] = Enum.filter(conn.req_headers, fn {k, _} ->
k == "accept-language" end)
[lang | _] = Enum.take(String.split(String.upcase(langs), ","), 1)
# remote_ip comes in as a Tuple {192, 168, 1, 42} >> 192.168.1.42 (dot quad)
ip = Enum.join(Tuple.to_list(conn.remote_ip), ".")
Enum.join([ua, ip, lang], "|")
end
@doc """
save_user_agent_hash/1 save/overwrite user-agent in a file
the filename is the SHA hash of the string so it's always the same.
from the Plug.Conn map see: https://hexdocs.pm/plug/Plug.Conn.html
> there is probably a *much* better way of doing this ... PR v. welcome!
## Parameters
- conn: Map the standard Plug.Conn info see: hexdocs.pm/plug/Plug.Conn.html
Returns String with user-agent, IP and language separated by "pipe" charater.
"""
def save_user_agent_hash(conn) do
ua = get_user_agent_string(conn)
hash = Hash.make(ua, 10)
agent_path = Path.expand("./logs") <> "/agents/" <> hash
File.write(agent_path, ua, [:binary])
hash
end
@doc """
get_hit_count/1 reads the log file for the given file path (if it exists)
finds the last line in the file and read the hit valie in the last column.
## Parameters
- hit_path: String the filesystem path to the hits append-only log file.
Returns Number count the current hit count for the given url.
"""
def get_hit_count(hit_path) do
# check if existing hits log for url
exists = File.regular?(hit_path)
count =
if exists do
stream = File.stream!(hit_path)
# take the last line in the file to get current count
[last_line] =
stream
|> Stream.map(&String.trim_trailing/1)
|> Enum.to_list()
|> Enum.take(-1)
# single element list
[i] = Enum.take(String.split(last_line, "|"), -1)
# for some reason Int.parse returns tuple...
{count, _} = Integer.parse(i)
# increment hit counter
count + 1
else
# no previous hits for this url so count is 1
1
end
count
end
@doc """
save_hit/1 is the "main" function for saving a hit including extracting
user-agent data from conn (see above).
## Parameters
- conn: Map the standard Plug.Conn info see: hexdocs.pm/plug/Plug.Conn.html
Returns Number count the current hit count for the given url.
"""
def save_hit(conn) do
path = conn.path_info
hit_path =
Path.expand("./logs") <> "/" <>
String.replace(Enum.join(path, "_"), ".svg", "") <> ".log"
count = get_hit_count(hit_path)
hash = save_user_agent_hash(conn)
svg_path = Enum.join(path, "/")
hit =
Enum.join(
[
Integer.to_string(System.system_time(:millisecond)),
svg_path,
hash,
count
],
"|"
) <> "\n"
File.write!(hit_path, hit, [:append])
# Broadcast to Connected Clients
broadcast(svg_path, hash, count)
count
end
@doc """
broadcast/3 submits the hit data for all the connected
websocket clients
## Parameters
- svg_path: full svg path
- hash: hash of the user/agent
- count: current hits count
Returns Number count the current hit count for the given url.
"""
def broadcast(svg_path, hash, count) do
time = App.Utils.now_to_string()
git_path = String.replace(svg_path, ".svg", "")
msg = Enum.join([time, git_path, count, hash], " ")
App.WebsocketServer.broadcast(msg)
end
end