litestream_rails 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +172 -13
- data/app/controllers/litestream_rails/application_controller.rb +7 -0
- data/app/controllers/litestream_rails/processes_controller.rb +9 -0
- data/app/views/layouts/litestream_rails/_style.html +269 -612
- data/app/views/layouts/litestream_rails/application.html.erb +3 -3
- data/app/views/litestream_rails/processes/show.html.erb +121 -0
- data/config/routes.rb +2 -2
- data/lib/litestream_rails/version.rb +1 -1
- data/lib/litestream_rails.rb +58 -13
- metadata +8 -8
- data/app/controllers/litestream_rails/databases_controller.rb +0 -32
- data/app/views/litestream_rails/databases/index.html.erb +0 -85
@@ -7,12 +7,12 @@
|
|
7
7
|
|
8
8
|
<%= render "layouts/litestream_rails/style" %>
|
9
9
|
</head>
|
10
|
-
<body class="h-full flex flex-col
|
11
|
-
<main class="
|
10
|
+
<body class="h-full flex flex-col">
|
11
|
+
<main class="container mx-auto max-w-4xl mt-4 px-2 grow">
|
12
12
|
<%= content_for?(:content) ? yield(:content) : yield %>
|
13
13
|
</main>
|
14
14
|
|
15
|
-
<footer class="container mx-auto
|
15
|
+
<footer class="container mx-auto mt-24 flex items-center justify-between border-t px-2 py-4 text-base">
|
16
16
|
<p>
|
17
17
|
<code><strong>Litestream</strong> <em>Rails</em></code> |
|
18
18
|
Made by <a href="https://twitter.com/fractaledmind" class="text-blue-500 hover:underline decoration-blue-500">@fractaledmind</a> and <a href="https://github.com/fractaledmind/litestream_rails/graphs/contributors" class="text-blue-500 hover:underline decoration-blue-500">friends</a>! Want to help? It's <a href="https://github.com/fractaledmind/litestream_rails" class="text-blue-500 hover:underline decoration-blue-500">open source</a>!
|
@@ -0,0 +1,121 @@
|
|
1
|
+
<section id="process_<%= @process[:pid] %>" class="space-y-6">
|
2
|
+
<div class="flex items-center justify-between">
|
3
|
+
<h1 class="flex items-baseline gap-2 text-2xl font-bold">
|
4
|
+
Litestream
|
5
|
+
|
6
|
+
<% if @process[:status] == "sleeping" %>
|
7
|
+
<small class="inline-flex rounded-full px-2 text-sm font-semibold bg-yellow-100 text-yellow-800">
|
8
|
+
<%= @process[:status] %>
|
9
|
+
</small>
|
10
|
+
<% elsif @process[:status] %>
|
11
|
+
<small class="inline-flex rounded-full px-2 text-sm font-semibold bg-green-100 text-green-800">
|
12
|
+
<%= @process[:status] %>
|
13
|
+
</small>
|
14
|
+
<% else %>
|
15
|
+
<small class="inline-flex rounded-full px-2 text-sm font-semibold bg-red-100 text-red-800">
|
16
|
+
not running
|
17
|
+
</small>
|
18
|
+
<% end %>
|
19
|
+
</h1>
|
20
|
+
|
21
|
+
<% if @process[:status] %>
|
22
|
+
<small class="text-base">
|
23
|
+
#<code><%= @process[:pid] %></code>
|
24
|
+
</small>
|
25
|
+
<% end %>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<% if @process[:status] %>
|
29
|
+
<dl class="grid grid-cols-[fit-content(100%)_1fr] gap-x-4">
|
30
|
+
<dt class="font-bold">Started at</dt>
|
31
|
+
<dd class="">
|
32
|
+
<abbr title="<%= @process[:started] %>" class="underline decoration-dashed decoration-gray-500 cursor-help">
|
33
|
+
<time datetime="<%= @process[:started] %>"><%= @process[:started].to_formatted_s(:db) %></time>
|
34
|
+
</abbr>
|
35
|
+
</dd>
|
36
|
+
</dl>
|
37
|
+
<% end %>
|
38
|
+
</section>
|
39
|
+
<br>
|
40
|
+
<br>
|
41
|
+
|
42
|
+
<section id="generations" class="">
|
43
|
+
<div class="mb-3 flex items-center justify-between border-b">
|
44
|
+
<h2 class="text-2xl font-bold">Databases</h2>
|
45
|
+
<p class="text-right">Total: <strong><%= @databases.size %></strong></p>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<ul class="list-[square] list-outside ml-4">
|
49
|
+
<% @databases.each do |database| %>
|
50
|
+
<li>
|
51
|
+
<div class="flex items-center justify-between">
|
52
|
+
<h2 class="text-lg font-bold">
|
53
|
+
<code><%= database['path'] %></code>
|
54
|
+
</h2>
|
55
|
+
<button type="button" class="rounded-md bg-slate-800 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-700">Restore</button>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<br />
|
59
|
+
<section id="generations" class="ml-6">
|
60
|
+
<% database['generations'].each do |generation| %>
|
61
|
+
<details id="<%= generation['generation'] %>" open="open">
|
62
|
+
<summary class="cursor-pointer rounded p-2 hover:bg-gray-50">
|
63
|
+
<code><%= generation['generation'] %></code>
|
64
|
+
(<em><%= generation['lag'] %> lag</em>)
|
65
|
+
</summary>
|
66
|
+
|
67
|
+
<dl class="ml-7 grid grid-cols-[fit-content(100%)_1fr] gap-x-4">
|
68
|
+
<dt class="font-bold">Start</dt>
|
69
|
+
<dd class="">
|
70
|
+
<abbr title="<%= generation['start'] %>" class="underline decoration-dashed decoration-gray-500 cursor-help">
|
71
|
+
<time datetime="<%= generation['start'] %>"><%= DateTime.parse(generation['start']).to_formatted_s(:db) %></time>
|
72
|
+
</abbr>
|
73
|
+
</dd>
|
74
|
+
|
75
|
+
<dt class="font-bold">End</dt>
|
76
|
+
<dd class="">
|
77
|
+
<abbr title="<%= generation['end'] %>" class="underline decoration-dashed decoration-gray-500 cursor-help">
|
78
|
+
<time datetime="<%= generation['end'] %>"><%= DateTime.parse(generation['end']).to_formatted_s(:db) %></time>
|
79
|
+
</abbr>
|
80
|
+
</dd>
|
81
|
+
|
82
|
+
<div class="col-span-2">
|
83
|
+
<dt class="font-bold">Snapshots</dt>
|
84
|
+
<dd class="">
|
85
|
+
<table class="min-w-full divide-y divide-gray-300">
|
86
|
+
<thead>
|
87
|
+
<tr>
|
88
|
+
<th scope="col" class="whitespace-nowrap px-2 py-2 text-left text-sm font-semibold text-gray-900">Created at</th>
|
89
|
+
<th scope="col" class="whitespace-nowrap px-2 py-2 text-right text-sm font-semibold text-gray-900">Index</th>
|
90
|
+
<th scope="col" class="whitespace-nowrap px-2 py-2 text-right text-sm font-semibold text-gray-900">Size</th>
|
91
|
+
</tr>
|
92
|
+
</thead>
|
93
|
+
|
94
|
+
<tbody class="bg-white">
|
95
|
+
<% generation['snapshots'].each do |snapshot| %>
|
96
|
+
<tr class="align-top even:bg-gray-50">
|
97
|
+
<td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900">
|
98
|
+
<abbr title="<%= snapshot['created'] %>" class="underline decoration-dashed decoration-gray-500 cursor-help">
|
99
|
+
<time datetime="<%= snapshot['created'] %>"><%= DateTime.parse(snapshot['created']).to_formatted_s(:db) %></time>
|
100
|
+
</abbr>
|
101
|
+
</td>
|
102
|
+
<td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 text-right">
|
103
|
+
<%= snapshot['index'] %>
|
104
|
+
</td>
|
105
|
+
<td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 text-right">
|
106
|
+
<%= number_to_human_size snapshot['size'] %>
|
107
|
+
</td>
|
108
|
+
</tr>
|
109
|
+
<% end %>
|
110
|
+
</tbody>
|
111
|
+
</table>
|
112
|
+
</dd>
|
113
|
+
</div>
|
114
|
+
</dl>
|
115
|
+
</details>
|
116
|
+
<% end %>
|
117
|
+
</section>
|
118
|
+
</li>
|
119
|
+
<% end %>
|
120
|
+
</ul>
|
121
|
+
</section>
|
data/config/routes.rb
CHANGED
data/lib/litestream_rails.rb
CHANGED
@@ -10,25 +10,77 @@ module LitestreamRails
|
|
10
10
|
|
11
11
|
class << self
|
12
12
|
# use method instead of attr_accessor to ensure
|
13
|
-
# this works if variable set after
|
13
|
+
# this works if variable set after LitestreamRails is loaded
|
14
14
|
def username
|
15
15
|
@username ||= ENV["LITESTREAMRAILS_USERNAME"] || @@username
|
16
16
|
end
|
17
17
|
|
18
18
|
# use method instead of attr_accessor to ensure
|
19
|
-
# this works if variable set after
|
19
|
+
# this works if variable set after LitestreamRails is loaded
|
20
20
|
def password
|
21
21
|
@password ||= ENV["LITESTREAMRAILS_PASSWORD"] || @@password
|
22
22
|
end
|
23
23
|
|
24
|
+
def replicate_process
|
25
|
+
info = {}
|
26
|
+
if !`which systemctl`.empty?
|
27
|
+
systemctl_status = `systemctl status litestream`.chomp
|
28
|
+
# ["● litestream.service - Litestream",
|
29
|
+
# " Loaded: loaded (/lib/systemd/system/litestream.service; enabled; vendor preset: enabled)",
|
30
|
+
# " Active: active (running) since Tue 2023-07-25 13:49:43 UTC; 8 months 24 days ago",
|
31
|
+
# " Main PID: 1179656 (litestream)",
|
32
|
+
# " Tasks: 9 (limit: 1115)",
|
33
|
+
# " Memory: 22.9M",
|
34
|
+
# " CPU: 10h 49.843s",
|
35
|
+
# " CGroup: /system.slice/litestream.service",
|
36
|
+
# " └─1179656 /usr/bin/litestream replicate",
|
37
|
+
# "",
|
38
|
+
# "Warning: some journal files were not opened due to insufficient permissions."]
|
39
|
+
systemctl_status.split("\n").each do |line|
|
40
|
+
line.strip!
|
41
|
+
if line.start_with?("Main PID:")
|
42
|
+
_key, value = line.split(":")
|
43
|
+
pid, _name = value.strip.split(" ")
|
44
|
+
info[:pid] = pid
|
45
|
+
elsif line.start_with?("Active:")
|
46
|
+
_key, value = line.split(":")
|
47
|
+
value, _ago = value.split(";")
|
48
|
+
status, timestamp = value.split(" since ")
|
49
|
+
info[:started] = DateTime.strptime(timestamp.strip, "%Y-%m-%d %H:%M:%S %Z")
|
50
|
+
info[:status] = status.split("(").first.strip
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
litestream_replicate_ps = `ps -a | grep litestream | grep replicate`.chomp
|
55
|
+
litestream_replicate_ps.split("\n").each do |line|
|
56
|
+
next unless line.include?("litestream replicate")
|
57
|
+
pid, * = line.split(" ")
|
58
|
+
info[:pid] = pid
|
59
|
+
state, _, lstart = `ps -o "state,lstart" #{pid}`.chomp.split("\n").last.partition(/\s+/)
|
60
|
+
|
61
|
+
info[:status] = case state[0]
|
62
|
+
when "I" then "idle"
|
63
|
+
when "R" then "running"
|
64
|
+
when "S" then "sleeping"
|
65
|
+
when "T" then "stopped"
|
66
|
+
when "U" then "uninterruptible"
|
67
|
+
when "Z" then "zombie"
|
68
|
+
end
|
69
|
+
info[:started] = DateTime.strptime(lstart.strip, "%a %b %d %H:%M:%S %Y")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
info
|
73
|
+
end
|
74
|
+
|
24
75
|
def databases
|
25
|
-
databases =
|
76
|
+
databases = Litestream::Commands.databases
|
26
77
|
|
27
78
|
databases.each do |db|
|
28
|
-
generations =
|
29
|
-
snapshots =
|
79
|
+
generations = Litestream::Commands.generations(db["path"])
|
80
|
+
snapshots = Litestream::Commands.snapshots(db["path"])
|
81
|
+
db["path"] = db["path"].gsub(Rails.root.to_s, "[ROOT]")
|
30
82
|
|
31
|
-
db[
|
83
|
+
db["generations"] = generations.map do |generation|
|
32
84
|
id = generation["generation"]
|
33
85
|
replica = generation["name"]
|
34
86
|
generation["snapshots"] = snapshots.select { |snapshot| snapshot["generation"] == id && snapshot["replica"] == replica }
|
@@ -37,12 +89,5 @@ module LitestreamRails
|
|
37
89
|
end
|
38
90
|
end
|
39
91
|
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
def text_table_to_hashes(string)
|
44
|
-
keys, *rows = string.split("\n").map { _1.split(/\s+/) }
|
45
|
-
rows.map { keys.zip(_1).to_h }
|
46
|
-
end
|
47
92
|
end
|
48
93
|
end
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: litestream_rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Margheim
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-05-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: litestream
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.6.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.6.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: actionpack
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -90,10 +90,10 @@ files:
|
|
90
90
|
- README.md
|
91
91
|
- Rakefile
|
92
92
|
- app/controllers/litestream_rails/application_controller.rb
|
93
|
-
- app/controllers/litestream_rails/
|
93
|
+
- app/controllers/litestream_rails/processes_controller.rb
|
94
94
|
- app/views/layouts/litestream_rails/_style.html
|
95
95
|
- app/views/layouts/litestream_rails/application.html.erb
|
96
|
-
- app/views/litestream_rails/
|
96
|
+
- app/views/litestream_rails/processes/show.html.erb
|
97
97
|
- config/routes.rb
|
98
98
|
- lib/litestream_rails.rb
|
99
99
|
- lib/litestream_rails/engine.rb
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module LitestreamRails
|
2
|
-
class DatabasesController < ApplicationController
|
3
|
-
around_action :force_english_locale!
|
4
|
-
|
5
|
-
# GET /databases
|
6
|
-
def index
|
7
|
-
@databases = LitestreamRails.databases
|
8
|
-
[{"path"=>"/Users/fractaled/Code/Playgrounds/wrocloverb/storage/production.sqlite3",
|
9
|
-
"replicas"=>"s3",
|
10
|
-
"generations"=>
|
11
|
-
[{"generation"=>"5f4341bc3d22d615",
|
12
|
-
"name"=>"s3",
|
13
|
-
"lag"=>"3s",
|
14
|
-
"start"=>"2024-04-17T19:48:09Z",
|
15
|
-
"end"=>"2024-04-17T19:48:09Z",
|
16
|
-
"snapshots"=>[{"index"=>"0", "size"=>"4645465", "created"=>"2024-04-17T19:48:09Z"}]},
|
17
|
-
{"generation"=>"a184b05a685589f3",
|
18
|
-
"name"=>"s3",
|
19
|
-
"lag"=>"19h46m52s",
|
20
|
-
"start"=>"2024-04-17T00:01:19Z",
|
21
|
-
"end"=>"2024-04-17T00:01:19Z",
|
22
|
-
"snapshots"=>[{"index"=>"1", "size"=>"4645465", "created"=>"2024-04-17T00:01:19Z"}]}]}]
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def force_english_locale!(&action)
|
28
|
-
I18n.with_locale(:en, &action)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
@@ -1,85 +0,0 @@
|
|
1
|
-
<ul>
|
2
|
-
<% @databases.each do |database| %>
|
3
|
-
<li>
|
4
|
-
<section id="error_12" class="space-y-6">
|
5
|
-
<div class="flex items-center justify-between">
|
6
|
-
<h1 class="flex items-center gap-2 text-2xl font-bold">
|
7
|
-
<code><%= database['path'] %></code>
|
8
|
-
</h1>
|
9
|
-
</div>
|
10
|
-
</section>
|
11
|
-
<br />
|
12
|
-
<section id="generations" class="-mx-2 mt-4 sm:mx-0 sm:rounded-md">
|
13
|
-
<div class="mb-3 flex items-center justify-between border-b">
|
14
|
-
<h2 class="text-2xl font-bold">Generations</h2>
|
15
|
-
<p class="text-right">Total: <strong><%= database['generations'].size %></strong></p>
|
16
|
-
</div>
|
17
|
-
|
18
|
-
<div class="min-w-full">
|
19
|
-
<% database['generations'].each do |generation| %>
|
20
|
-
<section id="<%= generation['generation'] %>" class="">
|
21
|
-
<details open="open">
|
22
|
-
<summary class="cursor-pointer rounded p-2 hover:bg-gray-50">
|
23
|
-
<code><%= generation['generation'] %></code>
|
24
|
-
(<em><%= generation['lag'] %> lag</em>)
|
25
|
-
</summary>
|
26
|
-
<div class="">
|
27
|
-
<dl class="grid flex-1 grid-cols-2 gap-x-4">
|
28
|
-
<div class="flex flex-wrap items-center justify-between gap-x-2">
|
29
|
-
<dt class="font-bold">Start</dt>
|
30
|
-
<dd class="inline-flex items-center gap-1">
|
31
|
-
<abbr title="<%= generation['start'] %>" class="cursor-help">
|
32
|
-
<time datetime="<%= generation['start'] %>"><%= DateTime.parse(generation['start']).to_formatted_s(:db) %></time>
|
33
|
-
</abbr>
|
34
|
-
</dd>
|
35
|
-
</div>
|
36
|
-
<div class="flex flex-wrap items-center justify-between gap-x-2">
|
37
|
-
<dt class="font-bold">End</dt>
|
38
|
-
<dd class="inline-flex items-center gap-1">
|
39
|
-
<abbr title="<%= generation['end'] %>" class="cursor-help">
|
40
|
-
<time datetime="<%= generation['end'] %>"><%= DateTime.parse(generation['end']).to_formatted_s(:db) %></time>
|
41
|
-
</abbr>
|
42
|
-
</dd>
|
43
|
-
</div>
|
44
|
-
<div class="col-span-2">
|
45
|
-
<dt class="font-bold">Snapshots</dt>
|
46
|
-
<dd class="">
|
47
|
-
<table class="min-w-full divide-y divide-gray-300">
|
48
|
-
<thead>
|
49
|
-
<tr>
|
50
|
-
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3">Created</th>
|
51
|
-
<th scope="col" class="px-3 py-3.5 text-right text-sm font-semibold text-gray-900">Index</th>
|
52
|
-
<th scope="col" class="px-3 py-3.5 text-right text-sm font-semibold text-gray-900">Size</th>
|
53
|
-
</tr>
|
54
|
-
</thead>
|
55
|
-
|
56
|
-
<tbody class="bg-white">
|
57
|
-
<% generation['snapshots'].each do |snapshot| %>
|
58
|
-
<tr class="align-top even:bg-gray-50">
|
59
|
-
<td scope="col" class="whitespace-nowrap px-3 py-4 pt-7 text-left text-gray-500">
|
60
|
-
<abbr title="<%= snapshot['created'] %>" class="cursor-help">
|
61
|
-
<time datetime="<%= snapshot['created'] %>"><%= DateTime.parse(snapshot['created']).to_formatted_s(:db) %></time>
|
62
|
-
</abbr>
|
63
|
-
</td>
|
64
|
-
<td scope="col" class="whitespace-nowrap px-3 py-4 pt-7 text-right text-gray-500">
|
65
|
-
<%= snapshot['index'] %>
|
66
|
-
</td>
|
67
|
-
<td scope="col" class="whitespace-nowrap px-3 py-4 pt-7 text-right text-gray-500">
|
68
|
-
<%= number_to_human_size snapshot['size'] %>
|
69
|
-
</td>
|
70
|
-
</tr>
|
71
|
-
<% end %>
|
72
|
-
</tbody>
|
73
|
-
</table>
|
74
|
-
</dd>
|
75
|
-
</div>
|
76
|
-
</dl>
|
77
|
-
</div>
|
78
|
-
</details>
|
79
|
-
</section>
|
80
|
-
<% end %>
|
81
|
-
</div>
|
82
|
-
</section>
|
83
|
-
</li>
|
84
|
-
<% end %>
|
85
|
-
</ul>
|