litestream_rails 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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>