litestream-aarch64-linux 0.12.0-aarch64-linux
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/LICENSE-DEPENDENCIES +205 -0
- data/README.md +511 -0
- data/Rakefile +14 -0
- data/app/controllers/litestream/application_controller.rb +19 -0
- data/app/controllers/litestream/processes_controller.rb +9 -0
- data/app/controllers/litestream/restorations_controller.rb +17 -0
- data/app/jobs/litestream/verification_job.rb +13 -0
- data/app/views/layouts/litestream/_style.html +804 -0
- data/app/views/layouts/litestream/application.html.erb +56 -0
- data/app/views/litestream/processes/show.html.erb +121 -0
- data/config/routes.rb +6 -0
- data/exe/aarch64-linux/litestream +0 -0
- data/exe/litestream +12 -0
- data/lib/litestream/commands.rb +157 -0
- data/lib/litestream/engine.rb +26 -0
- data/lib/litestream/generators/litestream/install_generator.rb +29 -0
- data/lib/litestream/generators/litestream/templates/config.yml.erb +21 -0
- data/lib/litestream/generators/litestream/templates/initializer.rb +33 -0
- data/lib/litestream/upstream.rb +14 -0
- data/lib/litestream/version.rb +3 -0
- data/lib/litestream.rb +166 -0
- data/lib/puma/plugin/litestream.rb +69 -0
- data/lib/tasks/litestream_tasks.rake +92 -0
- metadata +212 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html class="h-full">
|
3
|
+
<head>
|
4
|
+
<title>Litestream</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= render "layouts/litestream/style" %>
|
9
|
+
</head>
|
10
|
+
<body class="h-full flex flex-col">
|
11
|
+
<main class="container mx-auto max-w-4xl mt-4 px-2 grow">
|
12
|
+
<%= content_for?(:content) ? yield(:content) : yield %>
|
13
|
+
</main>
|
14
|
+
|
15
|
+
<footer class="container mx-auto mt-24 flex items-center justify-between border-t px-2 py-4 text-base">
|
16
|
+
<p>
|
17
|
+
<code><strong>Litestream</strong></code> |
|
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-ruby/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-ruby" class="text-blue-500 hover:underline decoration-blue-500">open source</a>!
|
19
|
+
</p>
|
20
|
+
</footer>
|
21
|
+
|
22
|
+
<div class="fixed top-0 left-0 right-0 text-center py-2">
|
23
|
+
<% if notice.present? %>
|
24
|
+
<p id="notice"
|
25
|
+
class="py-2 px-3 bg-green-50 text-green-500 font-medium rounded-lg inline-block"
|
26
|
+
data-controller="fade">
|
27
|
+
<%= notice.html_safe %>
|
28
|
+
</p>
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
<% if alert.present? %>
|
32
|
+
<p id="alert"
|
33
|
+
class="py-2 px-3 bg-red-50 text-red-500 font-medium rounded-lg inline-block"
|
34
|
+
data-controller="fade">
|
35
|
+
<%= alert.html_safe %>
|
36
|
+
</p>
|
37
|
+
<% end %>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<script nonce="<%= content_security_policy_nonce %>">
|
41
|
+
function fadeOut(element) {
|
42
|
+
element.classList.add('transition-opacity')
|
43
|
+
setTimeout(
|
44
|
+
() => {
|
45
|
+
element.classList.add('opacity-0')
|
46
|
+
element.remove()
|
47
|
+
},
|
48
|
+
5000
|
49
|
+
)
|
50
|
+
}
|
51
|
+
document.querySelectorAll('[data-controller="fade"]').forEach(element => {
|
52
|
+
fadeOut(element);
|
53
|
+
});
|
54
|
+
</script>
|
55
|
+
</body>
|
56
|
+
</html>
|
@@ -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="databases" 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_to "Restore", restorations_path, class: "rounded-md bg-slate-800 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-700", params: { database: database['path'] } %>
|
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
ADDED
Binary file
|
data/exe/litestream
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
# because rubygems shims assume a gem's executables are Ruby
|
3
|
+
|
4
|
+
require "litestream/commands"
|
5
|
+
|
6
|
+
begin
|
7
|
+
command = [Litestream::Commands.executable, *ARGV]
|
8
|
+
exec(*command)
|
9
|
+
rescue Litestream::Commands::UnsupportedPlatformException, Litestream::Commands::ExecutableNotFoundException => e
|
10
|
+
warn("ERROR: " + e.message)
|
11
|
+
exit 1
|
12
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require_relative "upstream"
|
2
|
+
require "logfmt"
|
3
|
+
|
4
|
+
module Litestream
|
5
|
+
module Commands
|
6
|
+
DEFAULT_DIR = File.expand_path(File.join(__dir__, "..", "..", "exe"))
|
7
|
+
GEM_NAME = "litestream"
|
8
|
+
|
9
|
+
# raised when the host platform is not supported by upstream litestream's binary releases
|
10
|
+
UnsupportedPlatformException = Class.new(StandardError)
|
11
|
+
|
12
|
+
# raised when the litestream executable could not be found where we expected it to be
|
13
|
+
ExecutableNotFoundException = Class.new(StandardError)
|
14
|
+
|
15
|
+
# raised when LITESTREAM_INSTALL_DIR does not exist
|
16
|
+
DirectoryNotFoundException = Class.new(StandardError)
|
17
|
+
|
18
|
+
# raised when a litestream command requires a database argument but it isn't provided
|
19
|
+
DatabaseRequiredException = Class.new(StandardError)
|
20
|
+
|
21
|
+
# raised when a litestream command fails
|
22
|
+
CommandFailedException = Class.new(StandardError)
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def platform
|
26
|
+
[:cpu, :os].map { |m| Gem::Platform.local.send(m) }.join("-")
|
27
|
+
end
|
28
|
+
|
29
|
+
def executable(exe_path: DEFAULT_DIR)
|
30
|
+
litestream_install_dir = ENV["LITESTREAM_INSTALL_DIR"]
|
31
|
+
if litestream_install_dir
|
32
|
+
if File.directory?(litestream_install_dir)
|
33
|
+
warn "NOTE: using LITESTREAM_INSTALL_DIR to find litestream executable: #{litestream_install_dir}"
|
34
|
+
exe_path = litestream_install_dir
|
35
|
+
exe_file = File.expand_path(File.join(litestream_install_dir, "litestream"))
|
36
|
+
else
|
37
|
+
raise DirectoryNotFoundException, <<~MESSAGE
|
38
|
+
LITESTREAM_INSTALL_DIR is set to #{litestream_install_dir}, but that directory does not exist.
|
39
|
+
MESSAGE
|
40
|
+
end
|
41
|
+
else
|
42
|
+
if Litestream::Upstream::NATIVE_PLATFORMS.keys.none? { |p| Gem::Platform.match_gem?(Gem::Platform.new(p), GEM_NAME) }
|
43
|
+
raise UnsupportedPlatformException, <<~MESSAGE
|
44
|
+
litestream-ruby does not support the #{platform} platform
|
45
|
+
Please install litestream following instructions at https://litestream.io/install
|
46
|
+
MESSAGE
|
47
|
+
end
|
48
|
+
|
49
|
+
exe_file = Dir.glob(File.expand_path(File.join(exe_path, "*", "litestream"))).find do |f|
|
50
|
+
Gem::Platform.match_gem?(Gem::Platform.new(File.basename(File.dirname(f))), GEM_NAME)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
if exe_file.nil? || !File.exist?(exe_file)
|
55
|
+
raise ExecutableNotFoundException, <<~MESSAGE
|
56
|
+
Cannot find the litestream executable for #{platform} in #{exe_path}
|
57
|
+
|
58
|
+
If you're using bundler, please make sure you're on the latest bundler version:
|
59
|
+
|
60
|
+
gem install bundler
|
61
|
+
bundle update --bundler
|
62
|
+
|
63
|
+
Then make sure your lock file includes this platform by running:
|
64
|
+
|
65
|
+
bundle lock --add-platform #{platform}
|
66
|
+
bundle install
|
67
|
+
|
68
|
+
See `bundle lock --help` output for details.
|
69
|
+
|
70
|
+
If you're still seeing this message after taking those steps, try running
|
71
|
+
`bundle config` and ensure `force_ruby_platform` isn't set to `true`. See
|
72
|
+
https://github.com/fractaledmind/litestream-ruby#check-bundle_force_ruby_platform
|
73
|
+
for more details.
|
74
|
+
MESSAGE
|
75
|
+
end
|
76
|
+
|
77
|
+
exe_file
|
78
|
+
end
|
79
|
+
|
80
|
+
def replicate(async: false, **argv)
|
81
|
+
execute("replicate", argv, async: async, tabled_output: false)
|
82
|
+
end
|
83
|
+
|
84
|
+
def restore(database, async: false, **argv)
|
85
|
+
raise DatabaseRequiredException, "database argument is required for restore command, e.g. litestream:restore -- --database=path/to/database.sqlite" if database.nil?
|
86
|
+
argv.stringify_keys!
|
87
|
+
|
88
|
+
execute("restore", argv, database, async: async, tabled_output: false)
|
89
|
+
end
|
90
|
+
|
91
|
+
def databases(async: false, **argv)
|
92
|
+
execute("databases", argv, async: async, tabled_output: true)
|
93
|
+
end
|
94
|
+
|
95
|
+
def generations(database, async: false, **argv)
|
96
|
+
raise DatabaseRequiredException, "database argument is required for generations command, e.g. litestream:generations -- --database=path/to/database.sqlite" if database.nil?
|
97
|
+
|
98
|
+
execute("generations", argv, database, async: async, tabled_output: true)
|
99
|
+
end
|
100
|
+
|
101
|
+
def snapshots(database, async: false, **argv)
|
102
|
+
raise DatabaseRequiredException, "database argument is required for snapshots command, e.g. litestream:snapshots -- --database=path/to/database.sqlite" if database.nil?
|
103
|
+
|
104
|
+
execute("snapshots", argv, database, async: async, tabled_output: true)
|
105
|
+
end
|
106
|
+
|
107
|
+
def wal(database, async: false, **argv)
|
108
|
+
raise DatabaseRequiredException, "database argument is required for wal command, e.g. litestream:wal -- --database=path/to/database.sqlite" if database.nil?
|
109
|
+
|
110
|
+
execute("wal", argv, database, async: async, tabled_output: true)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def execute(command, argv = {}, database = nil, async: false, tabled_output: false)
|
116
|
+
cmd = prepare(command, argv, database)
|
117
|
+
results = run(cmd, async: async, tabled_output: tabled_output)
|
118
|
+
|
119
|
+
if Array === results && results.one? && results[0]["level"] == "ERROR"
|
120
|
+
raise CommandFailedException, "Failed to execute `#{cmd.join(" ")}`; Reason: #{results[0]["error"]}"
|
121
|
+
else
|
122
|
+
results
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def prepare(command, argv = {}, database = nil)
|
127
|
+
ENV["LITESTREAM_REPLICA_BUCKET"] ||= Litestream.replica_bucket
|
128
|
+
ENV["LITESTREAM_ACCESS_KEY_ID"] ||= Litestream.replica_key_id
|
129
|
+
ENV["LITESTREAM_SECRET_ACCESS_KEY"] ||= Litestream.replica_access_key
|
130
|
+
|
131
|
+
args = {
|
132
|
+
"--config" => Rails.root.join("config", "litestream.yml").to_s
|
133
|
+
}.merge(argv.stringify_keys).to_a.flatten.compact
|
134
|
+
cmd = [executable, command, *args, database].compact
|
135
|
+
puts cmd.inspect if ENV["DEBUG"]
|
136
|
+
|
137
|
+
cmd
|
138
|
+
end
|
139
|
+
|
140
|
+
def run(cmd, async: false, tabled_output: false)
|
141
|
+
if async
|
142
|
+
# To release the resources of the Ruby process, just fork and exit.
|
143
|
+
# The forked process executes litestream and replaces itself.
|
144
|
+
exec(*cmd) if fork.nil?
|
145
|
+
else
|
146
|
+
stdout = `#{cmd.join(" ")}`.chomp
|
147
|
+
tabled_output ? text_table_to_hashes(stdout) : stdout.split("\n").map { Logfmt.parse(_1) }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def text_table_to_hashes(string)
|
152
|
+
keys, *rows = string.split("\n").map { _1.split(/\s+/) }
|
153
|
+
rows.map { keys.zip(_1).to_h }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/engine"
|
4
|
+
|
5
|
+
module Litestream
|
6
|
+
class Engine < ::Rails::Engine
|
7
|
+
isolate_namespace Litestream
|
8
|
+
|
9
|
+
config.litestream = ActiveSupport::OrderedOptions.new
|
10
|
+
|
11
|
+
# Load the `litestream:install` generator into the host Rails app
|
12
|
+
generators do
|
13
|
+
require_relative "generators/litestream/install_generator"
|
14
|
+
end
|
15
|
+
|
16
|
+
initializer "litestream.config" do
|
17
|
+
config.litestream.each do |name, value|
|
18
|
+
Litestream.public_send(:"#{name}=", value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
initializer "deprecator" do |app|
|
23
|
+
app.deprecators[:litestream] = Litestream.deprecator
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
|
5
|
+
module Litestream
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def copy_config_file
|
11
|
+
template "config.yml.erb", "config/litestream.yml"
|
12
|
+
end
|
13
|
+
|
14
|
+
def copy_initializer_file
|
15
|
+
template "initializer.rb", "config/initializers/litestream.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def production_sqlite_databases
|
21
|
+
ActiveRecord::Base
|
22
|
+
.configurations
|
23
|
+
.configs_for(env_name: "production", include_hidden: true)
|
24
|
+
.select { |config| ["sqlite3", "litedb"].include? config.adapter }
|
25
|
+
.map(&:database)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# This is the actual configuration file for litestream.
|
2
|
+
#
|
3
|
+
# You can either use the generated `config/initializers/litestream.rb`
|
4
|
+
# file to configure the litestream-ruby gem, which will populate these
|
5
|
+
# ENV variables when using the `rails litestream:replicate` command.
|
6
|
+
#
|
7
|
+
# Or, if you prefer, manually manage ENV variables and this configuration file.
|
8
|
+
# In that case, simply ensure that the ENV variables are set before running the
|
9
|
+
# `replicate` command.
|
10
|
+
#
|
11
|
+
# For more details, see: https://litestream.io/reference/config/
|
12
|
+
dbs:
|
13
|
+
<%- production_sqlite_databases.each do |database| -%>
|
14
|
+
- path: <%= database %>
|
15
|
+
replicas:
|
16
|
+
- type: s3
|
17
|
+
bucket: $LITESTREAM_REPLICA_BUCKET
|
18
|
+
path: <%= database %>
|
19
|
+
access-key-id: $LITESTREAM_ACCESS_KEY_ID
|
20
|
+
secret-access-key: $LITESTREAM_SECRET_ACCESS_KEY
|
21
|
+
<%- end -%>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Use this hook to configure the litestream-ruby gem.
|
2
|
+
# All configuration options will be available as environment variables, e.g.
|
3
|
+
# config.replica_bucket becomes LITESTREAM_REPLICA_BUCKET
|
4
|
+
# This allows you to configure Litestream using Rails encrypted credentials,
|
5
|
+
# or some other mechanism where the values are only available at runtime.
|
6
|
+
|
7
|
+
Rails.application.configure do
|
8
|
+
# An example of using Rails encrypted credentials to configure Litestream.
|
9
|
+
# litestream_credentials = Rails.application.credentials.litestream
|
10
|
+
|
11
|
+
# Replica-specific bucket location.
|
12
|
+
# This will be your bucket's URL without the `https://` prefix.
|
13
|
+
# For example, if you used DigitalOcean Spaces, your bucket URL could look like:
|
14
|
+
# https://myapp.fra1.digitaloceanspaces.com
|
15
|
+
# And so you should set your `replica_bucket` to:
|
16
|
+
# myapp.fra1.digitaloceanspaces.com
|
17
|
+
# Litestream supports Azure Blog Storage, Backblaze B2, DigitalOcean Spaces,
|
18
|
+
# Scaleway Object Storage, Google Cloud Storage, Linode Object Storage, and
|
19
|
+
# any SFTP server.
|
20
|
+
# In this example, we are using Rails encrypted credentials to store the URL to
|
21
|
+
# our storage provider bucket.
|
22
|
+
# config.litestream.replica_bucket = litestream_credentials&.replica_bucket
|
23
|
+
|
24
|
+
# Replica-specific authentication key.
|
25
|
+
# Litestream needs authentication credentials to access your storage provider bucket.
|
26
|
+
# In this example, we are using Rails encrypted credentials to store the access key ID.
|
27
|
+
# config.litestream.replica_key_id = litestream_credentials&.replica_key_id
|
28
|
+
|
29
|
+
# Replica-specific secret key.
|
30
|
+
# Litestream needs authentication credentials to access your storage provider bucket.
|
31
|
+
# In this example, we are using Rails encrypted credentials to store the secret access key.
|
32
|
+
# config.litestream.replica_access_key = litestream_credentials&.replica_access_key
|
33
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Litestream
|
2
|
+
module Upstream
|
3
|
+
VERSION = "v0.3.13"
|
4
|
+
|
5
|
+
# rubygems platform name => upstream release filename
|
6
|
+
NATIVE_PLATFORMS = {
|
7
|
+
"aarch64-linux" => "litestream-#{VERSION}-linux-arm64.tar.gz",
|
8
|
+
"arm64-darwin" => "litestream-#{VERSION}-darwin-arm64.zip",
|
9
|
+
"arm64-linux" => "litestream-#{VERSION}-linux-arm64.tar.gz",
|
10
|
+
"x86_64-darwin" => "litestream-#{VERSION}-darwin-amd64.zip",
|
11
|
+
"x86_64-linux" => "litestream-#{VERSION}-linux-amd64.tar.gz"
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
data/lib/litestream.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sqlite3"
|
4
|
+
|
5
|
+
module Litestream
|
6
|
+
VerificationFailure = Class.new(StandardError)
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_writer :configuration
|
10
|
+
|
11
|
+
def configuration
|
12
|
+
@configuration ||= Configuration.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def deprecator
|
16
|
+
@deprecator ||= ActiveSupport::Deprecation.new("0.12.0", "Litestream")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.configure
|
21
|
+
deprecator.warn(
|
22
|
+
"Configuring Litestream via Litestream.configure is deprecated. Use Rails.application.configure { config.litestream.* = ... } instead.",
|
23
|
+
caller
|
24
|
+
)
|
25
|
+
self.configuration ||= Configuration.new
|
26
|
+
yield(configuration)
|
27
|
+
end
|
28
|
+
|
29
|
+
class Configuration
|
30
|
+
attr_accessor :replica_bucket, :replica_key_id, :replica_access_key
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
mattr_writer :username, :password, :queue, :replica_bucket, :replica_key_id, :replica_access_key, :systemctl_command
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def verify!(database_path)
|
40
|
+
database = SQLite3::Database.new(database_path)
|
41
|
+
database.execute("CREATE TABLE IF NOT EXISTS _litestream_verification (id INTEGER PRIMARY KEY, uuid BLOB)")
|
42
|
+
sentinel = SecureRandom.uuid
|
43
|
+
database.execute("INSERT INTO _litestream_verification (uuid) VALUES (?)", [sentinel])
|
44
|
+
# give the Litestream replication process time to replicate the sentinel value
|
45
|
+
sleep 10
|
46
|
+
|
47
|
+
backup_path = "tmp/#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_#{sentinel}.sqlite3"
|
48
|
+
Litestream::Commands.restore(database_path, **{"-o" => backup_path})
|
49
|
+
|
50
|
+
backup = SQLite3::Database.new(backup_path)
|
51
|
+
result = backup.execute("SELECT 1 FROM _litestream_verification WHERE uuid = ? LIMIT 1", sentinel) # => [[1]] || []
|
52
|
+
|
53
|
+
raise VerificationFailure, "Verification failed for `#{database_path}`" if result.empty?
|
54
|
+
|
55
|
+
true
|
56
|
+
ensure
|
57
|
+
database.execute("DELETE FROM _litestream_verification WHERE uuid = ?", sentinel)
|
58
|
+
database.close
|
59
|
+
Dir.glob(backup_path + "*").each { |file| File.delete(file) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# use method instead of attr_accessor to ensure
|
63
|
+
# this works if variable set after Litestream is loaded
|
64
|
+
def username
|
65
|
+
ENV["LITESTREAM_USERNAME"] || @@username || "litestream"
|
66
|
+
end
|
67
|
+
|
68
|
+
def password
|
69
|
+
ENV["LITESTREAM_PASSWORD"] || @@password
|
70
|
+
end
|
71
|
+
|
72
|
+
def queue
|
73
|
+
ENV["LITESTREAM_QUEUE"] || @@queue || "default"
|
74
|
+
end
|
75
|
+
|
76
|
+
def replica_bucket
|
77
|
+
@@replica_bucket || configuration.replica_bucket
|
78
|
+
end
|
79
|
+
|
80
|
+
def replica_key_id
|
81
|
+
@@replica_key_id || configuration.replica_key_id
|
82
|
+
end
|
83
|
+
|
84
|
+
def replica_access_key
|
85
|
+
@@replica_access_key || configuration.replica_access_key
|
86
|
+
end
|
87
|
+
|
88
|
+
def systemctl_command
|
89
|
+
@@systemctl_command || "systemctl status litestream"
|
90
|
+
end
|
91
|
+
|
92
|
+
def replicate_process
|
93
|
+
info = {}
|
94
|
+
if !`which systemctl`.empty?
|
95
|
+
systemctl_status = `#{Litestream.systemctl_command}`.chomp
|
96
|
+
# ["● litestream.service - Litestream",
|
97
|
+
# " Loaded: loaded (/lib/systemd/system/litestream.service; enabled; vendor preset: enabled)",
|
98
|
+
# " Active: active (running) since Tue 2023-07-25 13:49:43 UTC; 8 months 24 days ago",
|
99
|
+
# " Main PID: 1179656 (litestream)",
|
100
|
+
# " Tasks: 9 (limit: 1115)",
|
101
|
+
# " Memory: 22.9M",
|
102
|
+
# " CPU: 10h 49.843s",
|
103
|
+
# " CGroup: /system.slice/litestream.service",
|
104
|
+
# " └─1179656 /usr/bin/litestream replicate",
|
105
|
+
# "",
|
106
|
+
# "Warning: some journal files were not opened due to insufficient permissions."]
|
107
|
+
systemctl_status.split("\n").each do |line|
|
108
|
+
line.strip!
|
109
|
+
if line.start_with?("Main PID:")
|
110
|
+
_key, value = line.split(":")
|
111
|
+
pid, _name = value.strip.split(" ")
|
112
|
+
info[:pid] = pid
|
113
|
+
elsif line.start_with?("Active:")
|
114
|
+
value, _ago = line.split(";")
|
115
|
+
status, timestamp = value.split(" since ")
|
116
|
+
info[:started] = DateTime.strptime(timestamp.strip, "%a %Y-%m-%d %H:%M:%S %Z")
|
117
|
+
status_match = status.match(%r{\((?<status>.*)\)})
|
118
|
+
info[:status] = status_match ? status_match[:status] : nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
else
|
122
|
+
litestream_replicate_ps = `ps -ax | grep litestream | grep replicate`.chomp
|
123
|
+
litestream_replicate_ps.split("\n").each do |line|
|
124
|
+
next unless line.include?("litestream replicate")
|
125
|
+
pid, * = line.split(" ")
|
126
|
+
info[:pid] = pid
|
127
|
+
state, _, lstart = `ps -o "state,lstart" #{pid}`.chomp.split("\n").last.partition(/\s+/)
|
128
|
+
|
129
|
+
info[:status] = case state[0]
|
130
|
+
when "I" then "idle"
|
131
|
+
when "R" then "running"
|
132
|
+
when "S" then "sleeping"
|
133
|
+
when "T" then "stopped"
|
134
|
+
when "U" then "uninterruptible"
|
135
|
+
when "Z" then "zombie"
|
136
|
+
end
|
137
|
+
info[:started] = DateTime.strptime(lstart.strip, "%a %b %d %H:%M:%S %Y")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
info
|
141
|
+
end
|
142
|
+
|
143
|
+
def databases
|
144
|
+
databases = Commands.databases
|
145
|
+
|
146
|
+
databases.each do |db|
|
147
|
+
generations = Commands.generations(db["path"])
|
148
|
+
snapshots = Commands.snapshots(db["path"])
|
149
|
+
db["path"] = db["path"].gsub(Rails.root.to_s, "[ROOT]")
|
150
|
+
|
151
|
+
db["generations"] = generations.map do |generation|
|
152
|
+
id = generation["generation"]
|
153
|
+
replica = generation["name"]
|
154
|
+
generation["snapshots"] = snapshots.select { |snapshot| snapshot["generation"] == id && snapshot["replica"] == replica }
|
155
|
+
.map { |s| s.slice("index", "size", "created") }
|
156
|
+
generation.slice("generation", "name", "lag", "start", "end", "snapshots")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
require_relative "litestream/version"
|
164
|
+
require_relative "litestream/upstream"
|
165
|
+
require_relative "litestream/commands"
|
166
|
+
require_relative "litestream/engine" if defined?(::Rails::Engine)
|