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.
@@ -7,12 +7,12 @@
7
7
 
8
8
  <%= render "layouts/litestream_rails/style" %>
9
9
  </head>
10
- <body class="h-full flex flex-col pb-4">
11
- <main class="grow container mx-auto mt-4">
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 py-4 mt-24 text-base flex justify-between items-center border-t">
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>&nbsp;&nbsp;|&nbsp;
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
@@ -1,5 +1,5 @@
1
1
  LitestreamRails::Engine.routes.draw do
2
- get "/" => "databases#index", :as => :root
2
+ get "/" => "processes#show", :as => :root
3
3
 
4
- resources :databases, only: [:index], path: ""
4
+ resource :process, only: [:show], path: ""
5
5
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LitestreamRails
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -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 SolidErrors is loaded
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 SolidErrors is loaded
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 = text_table_to_hashes(Litestream::Commands.databases(async: false))
76
+ databases = Litestream::Commands.databases
26
77
 
27
78
  databases.each do |db|
28
- generations = text_table_to_hashes(Litestream::Commands.generations(db["path"], async: false))
29
- snapshots = text_table_to_hashes(Litestream::Commands.snapshots(db["path"], async: false))
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['generations'] = generations.map do |generation|
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.1.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-04-17 00:00:00.000000000 Z
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.5.3
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.5.3
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/databases_controller.rb
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/databases/index.html.erb
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>