litestream-aarch64-linux 0.12.0-x86_64-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.
@@ -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>&nbsp;&nbsp;|&nbsp;
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
@@ -0,0 +1,6 @@
1
+ Litestream::Engine.routes.draw do
2
+ get "/" => "processes#show", :as => :root
3
+
4
+ resource :process, only: [:show], path: ""
5
+ resources :restorations, only: [:create]
6
+ end
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
Binary file
@@ -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
@@ -0,0 +1,3 @@
1
+ module Litestream
2
+ VERSION = "0.12.0"
3
+ 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)