litestream 0.11.2 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59bbfcde749c016674729dc5b9b677e56cc5b31ec46727d490a19d481b928d61
4
- data.tar.gz: 87812cd947c281c8212dd4bc414d7aad7e0e70d8d3ff2ffe3c4f07ec31c063fb
3
+ metadata.gz: 1e4638f801b8e05319b2390a63422edd6778944bb053324aefdbd43f98a2f15c
4
+ data.tar.gz: ced2aa3f3648042bf7f2211c02b17db670d0cb8a0c86902067ab08688954cb37
5
5
  SHA512:
6
- metadata.gz: 13543423827364cbdb44b83674047549ce88df604017d2fca338a739e9453c097c6fec04aba2bf3621d3f725ced03fa62c20e132671d4e5f47616a1b1bc384b0
7
- data.tar.gz: 52300ad4aa5f5b46026c9dbab2425461b3d68684a3e9cde5a462684366a90714971b3bd53afaa331eaf6fb8a754d78d0c4d6451d9769459cc621fc636de352a2
6
+ metadata.gz: be394032aadcf1ae817248ea8e83c1d23433f007f133aa6a0680813d5dd360895695654295bc8f3c91bcd6e8608a973d85f64cea9bae85f64ab6597b2e541f40
7
+ data.tar.gz: f6e3e6681736fc683bc855ca85c8124cc003c94096406d0127932fc38815541c244a6c1a59ac88ba57d2f0a4816dac30ce25fb4deca4db5563484d99e3a20d6e
data/README.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # litestream-ruby
2
2
 
3
+ <p>
4
+ <a href="https://rubygems.org/gems/litestream">
5
+ <img alt="GEM Version" src="https://img.shields.io/gem/v/litestream?color=168AFE&include_prereleases&logo=ruby&logoColor=FE1616">
6
+ </a>
7
+ <a href="https://rubygems.org/gems/litestream">
8
+ <img alt="GEM Downloads" src="https://img.shields.io/gem/dt/litestream?color=168AFE&logo=ruby&logoColor=FE1616">
9
+ </a>
10
+ <a href="https://github.com/testdouble/standard">
11
+ <img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
12
+ </a>
13
+ <a href="https://github.com/fractaledmind/litestream-ruby/actions/workflows/main.yml">
14
+ <img alt="Tests" src="https://github.com/fractaledmind/litestream-ruby/actions/workflows/main.yml/badge.svg" />
15
+ </a>
16
+ <a href="https://github.com/sponsors/fractaledmind">
17
+ <img alt="Sponsors" src="https://img.shields.io/github/sponsors/fractaledmind?color=eb4aaa&logo=GitHub%20Sponsors" />
18
+ </a>
19
+ <a href="https://ruby.social/@fractaledmind">
20
+ <img alt="Ruby.Social Follow" src="https://img.shields.io/mastodon/follow/109291299520066427?domain=https%3A%2F%2Fruby.social&label=%40fractaledmind&style=social">
21
+ </a>
22
+ <a href="https://twitter.com/fractaledmind">
23
+ <img alt="Twitter Follow" src="https://img.shields.io/twitter/url?label=%40fractaledmind&style=social&url=https%3A%2F%2Ftwitter.com%2Ffractaledmind">
24
+ </a>
25
+ </p>
26
+
3
27
  [Litestream](https://litestream.io/) is a standalone streaming replication tool for SQLite. This gem provides a Ruby interface to Litestream.
4
28
 
5
29
  ## Installation
@@ -28,10 +52,11 @@ This gem wraps the standalone executable version of the [Litestream](https://lit
28
52
 
29
53
  Supported platforms are:
30
54
 
31
- * arm64-darwin (macos-arm64)
32
- * x86_64-darwin (macos-x64)
33
- * arm64-linux (linux-arm64)
34
- * x86_64-linux (linux-x64)
55
+ - arm64-darwin (macos-arm64)
56
+ - x86_64-darwin (macos-x64)
57
+ - aarch64-linux (linux-aarch64)
58
+ - arm64-linux (linux-arm64)
59
+ - x86_64-linux (linux-x64)
35
60
 
36
61
  ### Using a local installation of `litestream`
37
62
 
@@ -39,13 +64,13 @@ If you are not able to use the vendored standalone executables (for example, if
39
64
 
40
65
  For example, if you've installed `litestream` so that the executable is found at `/usr/local/bin/litestream`, then you should set your environment variable like so:
41
66
 
42
- ``` sh
67
+ ```sh
43
68
  LITESTREAM_INSTALL_DIR=/usr/local/bin
44
69
  ```
45
70
 
46
71
  This also works with relative paths. If you've installed into your app's directory at `./.bin/litestream`:
47
72
 
48
- ``` sh
73
+ ```sh
49
74
  LITESTREAM_INSTALL_DIR=.bin
50
75
  ```
51
76
 
@@ -68,31 +93,33 @@ dbs:
68
93
  secret-access-key: $LITESTREAM_SECRET_ACCESS_KEY
69
94
  ```
70
95
 
71
- The gem also provides a default initializer file at `config/initializers/litestream.rb` that allows you to configure these four environment variables referenced in the configuration file in Ruby. By providing a Ruby interface to these environment variables, you can use any method of storing secrets that you prefer. For example, the default generated file uses Rails' encrypted credentials to store your secrets:
96
+ This is the default for Amazon S3. The full range of possible replica types (e.g. other S3-compatible object storage servers) are covered in Litestream's [replica guides](https://litestream.io/guides/#replica-guides).
97
+
98
+ The gem also provides a default initializer file at `config/initializers/litestream.rb` that allows you to configure these three environment variables referenced in the configuration file in Ruby. By providing a Ruby interface to these environment variables, you can use any method of storing secrets that you prefer. For example, the default generated file uses Rails' encrypted credentials to store your secrets:
72
99
 
73
100
  ```ruby
74
- litestream_credentials = Rails.application.credentials.litestream
75
- Litestream.configure do |config|
76
- config.replica_bucket = litestream_credentials.replica_bucket
77
- config.replica_key_id = litestream_credentials.replica_key_id
78
- config.replica_access_key = litestream_credentials.replica_access_key
101
+ Rails.application.configure do
102
+ litestream_credentials = Rails.application.credentials.litestream
103
+
104
+ config.litestream.replica_bucket = litestream_credentials&.replica_bucket
105
+ config.litestream.replica_key_id = litestream_credentials&.replica_key_id
106
+ config.litestream.replica_access_key = litestream_credentials&.replica_access_key
79
107
  end
80
108
  ```
81
109
 
82
- However, if you need manual control over the Litestream configuration, you can manually edit the `config/litestream.yml` file. The full range of possible configurations are covered in Litestream's [configuration reference](https://litestream.io/reference/config/).
110
+ However, if you need manual control over the Litestream configuration, you can manually edit the `config/litestream.yml` file. The full range of possible configurations are covered in Litestream's [configuration reference](https://litestream.io/reference/config/). NB: If you configure a longer `sync-interval`, you may need to adjust `replication_sleep` when calling `Litestream.verify!`.
83
111
 
84
112
  ### Replication
85
113
 
86
114
  In order to stream changes to your configured replicas, you need to start the Litestream replication process.
87
115
 
88
- The simplest way to run the Litestream replication process is use the Puma plugin provided by the gem. This allows you to run the Litestream replication process together with Puma and have Puma monitor and manage it. You just need to add
116
+ The simplest way to run the Litestream replication process is use the Puma plugin provided by the gem. This allows you to run the Litestream replication process together with Puma and have Puma monitor and manage it. You just need to add the following to your `puma.rb` configuration:
89
117
 
90
118
  ```ruby
91
- plugin :litestream
119
+ # Run litestream only in production.
120
+ plugin :litestream if ENV.fetch("RAILS_ENV", "production") == "production"
92
121
  ```
93
122
 
94
- to your `puma.rb` configuration.
95
-
96
123
  If you would prefer to run the Litestream replication process separately from Puma, you can use the provided `litestream:replicate` rake task. This rake task will automatically load the configuration file and set the environment variables before starting the Litestream process.
97
124
 
98
125
  The simplest way to spin up a Litestream process separately from your Rails application is to use a `Procfile`:
@@ -130,20 +157,32 @@ The Litestream `replicate` command supports the following options, which can be
130
157
 
131
158
  ### Restoration
132
159
 
133
- You can restore any replicated database at any point using the gem's provided `litestream:restore` rake task. This rake task requires that you specify which specific database you want to restore. As with the `litestream:replicate` task, you pass arguments to the rake task via argument forwarding. For example, to restore the production database, you would run:
160
+ You can restore any replicated database at any point using the gem's provided `litestream:restore` rake task. This rake task requires that you specify which specific database you want to restore. As with the `litestream:replicate` task, you pass arguments to the rake task via argument forwarding. For example, to restore the production database, you would do the following:
134
161
 
135
- ```shell
136
- bin/rails litestream:restore -- --database=storage/production.sqlite3
137
- # or
138
- bundle exec rake litestream:restore -- --database=storage/production.sqlite3
139
- ```
162
+ > [!NOTE]
163
+ > During the restoration process, you need to prevent any interaction with ActiveRecord/SQLite, such as from a running `rails server` or `rails console` instance. If there is any interaction, Rails might regenerate the production database and prevent restoration via litestream. If this happens, you might get a "cannot restore, output path already exists" error.
164
+
165
+ 1. Rename the production (`production.sqlite3`, `production.sqlite3-shm`, and `production.sqlite3-wal`) databases (**recommended**) or alternatively delete. To delete the production databases locally, you can run the following at your own risk:
166
+ ```shell
167
+ # DANGEROUS OPERATION, consider renaming database files instead
168
+ bin/rails db:drop DISABLE_DATABASE_ENVIRONMENT_CHECK=1
169
+ ```
170
+
171
+ 2. Run restore command:
172
+ ```shell
173
+ bin/rails litestream:restore -- --database=storage/production.sqlite3
174
+ # or
175
+ bundle exec rake litestream:restore -- --database=storage/production.sqlite3
176
+ ```
177
+
178
+ 3. Restart your Rails application or Docker container if applicable.
140
179
 
141
180
  You can restore any of the databases specified in your `config/litestream.yml` file. The `--database` argument should be the path to the database file you want to restore and must match the value for the `path` key of one of your configured databases. The `litestream:restore` rake task will automatically load the configuration file and set the environment variables before calling the Litestream executable.
142
181
 
143
182
  If you need to pass arguments through the rake task to the underlying `litestream` command, that can be done with additional forwarded arguments:
144
183
 
145
184
  ```shell
146
- bin/rails litestream:replicate -- --database=storage/production.sqlite3 --if-db-not-exists
185
+ bin/rails litestream:restore -- --database=storage/production.sqlite3 --if-db-not-exists
147
186
  ```
148
187
 
149
188
  You can forward arguments in whatever order you like, you simply need to ensure that the `--database` argument is present. You can also use either a single-dash `-database` or double-dash `--database` argument format. The Litestream `restore` command supports the following options, which can be passed through the rake task:
@@ -195,7 +234,7 @@ You can verify the integrity of your backed-up databases using the gem's provide
195
234
  Litestream.verify! "storage/production.sqlite3"
196
235
  ```
197
236
 
198
- In order to verify that the backup for that database is both restorable and fresh, the method will add a new row to that database under the `_litestream_verification` table, which it will create if needed. It will then wait 10 seconds to give the Litestream utility time to replicate that change to whatever storage providers you have configured. After that, it will download the latest backup from that storage provider and ensure that this verification row is present in the backup. If the verification row is _not_ present, the method will raise a `Litestream::VerificationFailure` exception. This check ensures that the restored database file
237
+ In order to verify that the backup for that database is both restorable and fresh, the method will add a new row to that database under the `_litestream_verification` table, which it will create if needed. It will then wait `replication_sleep` seconds (defaults to 10) to give the Litestream utility time to replicate that change to whatever storage providers you have configured. After that, it will download the latest backup from that storage provider and ensure that this verification row is present in the backup. If the verification row is _not_ present, the method will raise a `Litestream::VerificationFailure` exception. This check ensures that the restored database file:
199
238
 
200
239
  1. exists,
201
240
  2. can be opened by SQLite, and
@@ -203,6 +242,119 @@ In order to verify that the backup for that database is both restorable and fres
203
242
 
204
243
  After restoring the backup, the `Litestream.verify!` method will delete the restored database file. If you need the restored database file, use the `litestream:restore` rake task or `Litestream::Commands.restore` method instead.
205
244
 
245
+ ### Dashboard
246
+
247
+ The gem provides a web dashboard for monitoring the status of your Litestream replication. To mount the dashboard in your Rails application, add the following to your `config/routes.rb` file:
248
+
249
+ ```ruby
250
+ authenticate :user, -> (user) { user.admin? } do
251
+ mount Litestream::Engine, at: "/litestream"
252
+ end
253
+ ```
254
+
255
+ > [!NOTE]
256
+ > Be sure to [secure the dashboard](#authentication) in production.
257
+
258
+ #### Authentication
259
+
260
+ Litestream Rails does not restrict access out of the box. You must secure the dashboard yourself. However, it does provide basic HTTP authentication that can be used with basic authentication or Devise. All you need to do is setup a username and password.
261
+
262
+ There are two ways to setup a username and password. First, you can use the `LITESTREAM_USERNAME` and `LITESTREAM_PASSWORD` environment variables:
263
+
264
+ ```ruby
265
+ ENV["LITESTREAM_USERNAME"] = "frodo"
266
+ ENV["LITESTREAM_PASSWORD"] = "ikeptmysecrets"
267
+ ```
268
+
269
+ Second, you can configure the access credentials via the Rails configuration object, under the `litestream` key, in an initializer:
270
+
271
+ ```ruby
272
+ # Set authentication credentials for Litestream
273
+ config.litestream.username = Rails.application.credentials.dig(:litestream, :username)
274
+ config.litestream.password = Rails.application.credentials.dig(:litestream, :password)
275
+ ```
276
+
277
+ Either way, if you have set a username and password, Litestream will use basic HTTP authentication.
278
+
279
+ > [!IMPORTANT]
280
+ > If you have not set a username and password, Litestream will not require any authentication to view the dashboard.
281
+
282
+ If you use Devise for authentication in your app, you can also restrict access to the dashboard by using their `authenticate` constraint in your routes file:
283
+
284
+ ```ruby
285
+ authenticate :user, -> (user) { user.admin? } do
286
+ mount Litestream::Engine, at: "/litestream"
287
+ end
288
+ ```
289
+
290
+ ### Examples
291
+
292
+ There is only one screen in the dashboard.
293
+
294
+ - the show view of the Litestream replication process:
295
+
296
+ ![screenshot of the single page in the web dashboard, showing details of the Litestream replication process](images/show-screenshot.png)
297
+
298
+ ### Usage with API-only Applications
299
+
300
+ If your Rails application is an API-only application (generated with the `rails new --api` command), you will need to add the following middleware to your `config/application.rb` file in order to use the dashboard UI provided by Litestream:
301
+
302
+ ```ruby
303
+ # /config/application.rb
304
+ config.middleware.use ActionDispatch::Cookies
305
+ config.middleware.use ActionDispatch::Session::CookieStore
306
+ config.middleware.use ActionDispatch::Flash
307
+ ```
308
+
309
+ ### Overwriting the views
310
+
311
+ You can find the views in [`app/views`](https://github.com/fractaledmind/litestream-ruby/tree/main/app/views).
312
+
313
+ ```bash
314
+ app/views/
315
+ ├── layouts
316
+ │   └── litestream
317
+ │   ├── _style.html
318
+ │   └── application.html.erb
319
+ └── litestream
320
+ └── processes
321
+    └── show.html.erb
322
+ ```
323
+
324
+ You can always take control of the views by creating your own views and/or partials at these paths in your application. For example, if you wanted to overwrite the application layout, you could create a file at `app/views/layouts/litestream/application.html.erb`. If you wanted to remove the footer and the automatically disappearing flash messages, as one concrete example, you could define that file as:
325
+
326
+ ```erb
327
+ <!DOCTYPE html>
328
+ <html>
329
+ <head>
330
+ <title>Litestream</title>
331
+ <%= csrf_meta_tags %>
332
+ <%= csp_meta_tag %>
333
+
334
+ <%= render "layouts/litestream/style" %>
335
+ </head>
336
+ <body class="h-full flex flex-col">
337
+ <main class="container mx-auto max-w-4xl mt-4 px-2 grow">
338
+ <%= content_for?(:content) ? yield(:content) : yield %>
339
+ </main>
340
+
341
+ <div class="fixed top-0 left-0 right-0 text-center py-2">
342
+ <% if notice.present? %>
343
+ <p class="py-2 px-3 bg-green-50 text-green-500 font-medium rounded-lg inline-block">
344
+ <%= notice %>
345
+ </p>
346
+ <% end %>
347
+
348
+ <% if alert.present? %>
349
+ <p class="py-2 px-3 bg-red-50 text-red-500 font-medium rounded-lg inline-block">
350
+ <%= alert %>
351
+ </p>
352
+ <% end %>
353
+ </div>
354
+ </body>
355
+ </html>
356
+ ```
357
+
206
358
  ### Introspection
207
359
 
208
360
  Litestream offers a handful of commands that allow you to introspect the state of your replication. The gem provides a few rake tasks that wrap these commands for you. For example, you can list the databases that Litestream is configured to replicate:
@@ -224,14 +376,14 @@ You can also list the generations of a specific database:
224
376
  bin/rails litestream:generations -- --database=storage/production.sqlite3
225
377
  ```
226
378
 
227
- This will list all generations for the specified database, including stats about their lag behind the primary database and the time range they cover.
379
+ This will list all generations for the specified database, including stats about their lag behind the primary database and the time range they cover:
228
380
 
229
381
  ```
230
382
  name generation lag start end
231
383
  s3 a295b16a796689f3 -156ms 2024-04-17T00:01:19Z 2024-04-17T00:01:19Z
232
384
  ```
233
385
 
234
- Finally, you can list the snapshots available for a database:
386
+ You can list the snapshots available for a database:
235
387
 
236
388
  ```shell
237
389
  bin/rails litestream:snapshots -- --database=storage/production.sqlite3
@@ -244,6 +396,19 @@ replica generation index size created
244
396
  s3 a295b16a796689f3 1 4645465 2024-04-17T00:01:19Z
245
397
  ```
246
398
 
399
+ Finally, you can list the wal files available for a database:
400
+
401
+ ```shell
402
+ bin/rails litestream:wal -- --database=storage/production.sqlite3
403
+ ```
404
+
405
+ This command lists wal files available for that specified database:
406
+
407
+ ```
408
+ replica generation index offset size created
409
+ s3 a295b16a796689f3 1 0 2036 2024-04-17T00:01:19Z
410
+ ```
411
+
247
412
  ### Running commands from Ruby
248
413
 
249
414
  In addition to the provided rake tasks, you can also run Litestream commands directly from Ruby. The gem provides a `Litestream::Commands` module that wraps the Litestream CLI commands. This is particularly useful for the introspection commands, as you can use the output in your Ruby code.
@@ -269,18 +434,18 @@ Litestream::Commands.snapshots('storage/production.sqlite3')
269
434
  # => [{"replica"=>"s3", "generation"=>"5f4341bc3d22d615", "index"=>"0", "size"=>"4645465", "created"=>"2024-04-17T19:48:09Z"}]
270
435
  ```
271
436
 
272
- You can also restore a database programatically using the `Litestream::Commands.restore` method, which returns the path to the restored database:
437
+ The `Litestream::Commands.wal` method returns an array of hashes with the "replica", "generation", "index", "offset","size", and "created" keys for each wal:
273
438
 
274
439
  ```ruby
275
- Litestream::Commands.restore('storage/production.sqlite3')
276
- # => "storage/production-20240418090048.sqlite3"
440
+ Litestream::Commands.wal('storage/production.sqlite3')
441
+ # => [{"replica"=>"s3", "generation"=>"5f4341bc3d22d615", "index"=>"0", "offset"=>"0", "size"=>"2036", "created"=>"2024-04-17T19:48:09Z"}]
277
442
  ```
278
443
 
279
- Finally, you can verify the integrity of a restored database using the `Litestream::Commands.verify` method, which returns a hash with the "size" and "tables" keys for the original and restored databases:
444
+ You can also restore a database programmatically using the `Litestream::Commands.restore` method, which returns the path to the restored database:
280
445
 
281
446
  ```ruby
282
- Litestream::Commands.verify('storage/production.sqlite3')
283
- # => {"size"=>{"original"=>21688320, "restored"=>21688320}, "tables"=>{"original"=>9, "restored"=>9}}
447
+ Litestream::Commands.restore('storage/production.sqlite3')
448
+ # => "storage/production-20240418090048.sqlite3"
284
449
  ```
285
450
 
286
451
  You _can_ start the replication process using the `Litestream::Commands.replicate` method, but this is not recommended. The replication process should be managed by Litestream itself, and you should not need to manually start it.
@@ -303,7 +468,13 @@ litestream wal [arguments] DB_PATH|REPLICA_URL
303
468
 
304
469
  ### Using in development
305
470
 
306
- By default, installing the gem does not update your `Procfile.dev` file, and so Litestream will not be started in development. If you would like to test that your configuration is properly setup, you can manually add the `litestream:replicate` rake task to your `Procfile.dev` file. Just copy the `litestream` definition from the production `Procfile`. Then, in order to have a replication bucket for Litestream to point to, you can use a Docker instance of [MinIO](https://min.io/). MinIO is an S3-compatible object storage server that can be run locally. You can run a MinIO server with the following command:
471
+ By default, if you install the gem and configure via `puma.rb` or `Procfile`, Litestream will not start in development.
472
+
473
+ If you setup via `puma.rb`, then remove the conditional statement.
474
+
475
+ If you setup via `Procfile`, you will need to update your `Procfile.dev` file. If you would like to test that your configuration is properly setup, you can manually add the `litestream:replicate` rake task to your `Procfile.dev` file. Just copy the `litestream` definition from the production `Procfile`.
476
+
477
+ In order to have a replication bucket for Litestream to point to, you can use a Docker instance of [MinIO](https://min.io/). MinIO is an S3-compatible object storage server that can be run locally. You can run a MinIO server with the following command:
307
478
 
308
479
  ```sh
309
480
  docker run -p 9000:9000 -p 9001:9001 minio/minio server /data --console-address ":9001"
@@ -1,7 +1,6 @@
1
1
  module Litestream
2
- class ApplicationController < ActionController::Base
2
+ class ApplicationController < Litestream.base_controller_class.constantize
3
3
  protect_from_forgery with: :exception
4
- around_action :force_english_locale!
5
4
 
6
5
  if Litestream.password
7
6
  http_basic_authenticate_with(
@@ -9,11 +8,5 @@ module Litestream
9
8
  password: Litestream.password
10
9
  )
11
10
  end
12
-
13
- private
14
-
15
- def force_english_locale!(&action)
16
- I18n.with_locale(:en, &action)
17
- end
18
11
  end
19
12
  end
@@ -801,4 +801,42 @@
801
801
  .hover\:underline:hover {
802
802
  text-decoration-line: underline;
803
803
  }
804
+ @media (prefers-color-scheme: dark) {
805
+ .dark\:border-gray-800 {
806
+ --tw-border-opacity: 1;
807
+ border-color: rgb(31 41 55 / var(--tw-border-opacity));
808
+ }
809
+ .dark\:bg-gray-900 {
810
+ --tw-bg-opacity: 1;
811
+ background-color: rgb(17 24 39 / var(--tw-bg-opacity));
812
+ }
813
+ .dark\:bg-green-900 {
814
+ --tw-bg-opacity: 1;
815
+ background-color: rgb(20 83 45 / var(--tw-bg-opacity));
816
+ }
817
+ .dark\:bg-red-900 {
818
+ --tw-bg-opacity: 1;
819
+ background-color: rgb(127 29 29 / var(--tw-bg-opacity));
820
+ }
821
+ .dark\:text-gray-100 {
822
+ --tw-text-opacity: 1;
823
+ color: rgb(243 244 246 / var(--tw-text-opacity));
824
+ }
825
+ .dark\:text-white {
826
+ --tw-text-opacity: 1;
827
+ color: rgb(255 255 255 / var(--tw-text-opacity));
828
+ }
829
+ .dark\:even\:bg-gray-800:nth-child(even) {
830
+ --tw-bg-opacity: 1;
831
+ background-color: rgb(31 41 55 / var(--tw-bg-opacity));
832
+ }
833
+ .dark\:hover\:bg-gray-900:hover {
834
+ --tw-bg-opacity: 1;
835
+ background-color: rgb(17 24 39 / var(--tw-bg-opacity));
836
+ }
837
+ .dark\:hover\:bg-gray-800:hover {
838
+ --tw-bg-opacity: 1;
839
+ background-color: rgb(31 41 55 / var(--tw-bg-opacity));
840
+ }
841
+ }
804
842
  </style>
@@ -7,22 +7,22 @@
7
7
 
8
8
  <%= render "layouts/litestream/style" %>
9
9
  </head>
10
- <body class="h-full flex flex-col">
10
+ <body class="h-full flex flex-col dark:bg-gray-900 dark:text-white">
11
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 mt-24 flex items-center justify-between border-t px-2 py-4 text-base">
15
+ <footer class="container mx-auto mt-24 flex items-center justify-between border-t dark:border-gray-800 px-2 py-4 text-base">
16
16
  <p>
17
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/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" class="text-blue-500 hover:underline decoration-blue-500">open source</a>!
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
19
  </p>
20
20
  </footer>
21
21
 
22
22
  <div class="fixed top-0 left-0 right-0 text-center py-2">
23
23
  <% if notice.present? %>
24
24
  <p id="notice"
25
- class="py-2 px-3 bg-green-50 text-green-500 font-medium rounded-lg inline-block"
25
+ class="py-2 px-3 bg-green-50 dark:bg-green-900 text-green-500 font-medium rounded-lg inline-block"
26
26
  data-controller="fade">
27
27
  <%= notice.html_safe %>
28
28
  </p>
@@ -30,7 +30,7 @@
30
30
 
31
31
  <% if alert.present? %>
32
32
  <p id="alert"
33
- class="py-2 px-3 bg-red-50 text-red-500 font-medium rounded-lg inline-block"
33
+ class="py-2 px-3 bg-red-50 dark:bg-red-900 text-red-500 font-medium rounded-lg inline-block"
34
34
  data-controller="fade">
35
35
  <%= alert.html_safe %>
36
36
  </p>
@@ -40,7 +40,7 @@
40
40
  <br>
41
41
 
42
42
  <section id="databases" class="">
43
- <div class="mb-3 flex items-center justify-between border-b">
43
+ <div class="mb-3 flex items-center justify-between border-b dark:border-gray-800">
44
44
  <h2 class="text-2xl font-bold">Databases</h2>
45
45
  <p class="text-right">Total: <strong><%= @databases.size %></strong></p>
46
46
  </div>
@@ -59,7 +59,7 @@
59
59
  <section id="generations" class="ml-6">
60
60
  <% database['generations'].each do |generation| %>
61
61
  <details id="<%= generation['generation'] %>" open="open">
62
- <summary class="cursor-pointer rounded p-2 hover:bg-gray-50">
62
+ <summary class="cursor-pointer rounded p-2 hover:bg-gray-50 dark:hover:bg-gray-800">
63
63
  <code><%= generation['generation'] %></code>
64
64
  (<em><%= generation['lag'] %> lag</em>)
65
65
  </summary>
@@ -85,24 +85,24 @@
85
85
  <table class="min-w-full divide-y divide-gray-300">
86
86
  <thead>
87
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>
88
+ <th scope="col" class="whitespace-nowrap px-2 py-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Created at</th>
89
+ <th scope="col" class="whitespace-nowrap px-2 py-2 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">Index</th>
90
+ <th scope="col" class="whitespace-nowrap px-2 py-2 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">Size</th>
91
91
  </tr>
92
92
  </thead>
93
93
 
94
- <tbody class="bg-white">
94
+ <tbody class="bg-white dark:bg-gray-900">
95
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">
96
+ <tr class="align-top even:bg-gray-50 dark:even:bg-gray-800">
97
+ <td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100">
98
98
  <abbr title="<%= snapshot['created'] %>" class="underline decoration-dashed decoration-gray-500 cursor-help">
99
99
  <time datetime="<%= snapshot['created'] %>"><%= DateTime.parse(snapshot['created']).to_formatted_s(:db) %></time>
100
100
  </abbr>
101
101
  </td>
102
- <td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 text-right">
102
+ <td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100 text-right">
103
103
  <%= snapshot['index'] %>
104
104
  </td>
105
- <td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 text-right">
105
+ <td scope="col" class="whitespace-nowrap px-2 py-2 text-sm text-gray-900 dark:text-gray-100 text-right">
106
106
  <%= number_to_human_size snapshot['size'] %>
107
107
  </td>
108
108
  </tr>
@@ -104,6 +104,12 @@ module Litestream
104
104
  execute("snapshots", argv, database, async: async, tabled_output: true)
105
105
  end
106
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
+
107
113
  private
108
114
 
109
115
  def execute(command, argv = {}, database = nil, async: false, tabled_output: false)
@@ -119,11 +125,13 @@ module Litestream
119
125
 
120
126
  def prepare(command, argv = {}, database = nil)
121
127
  ENV["LITESTREAM_REPLICA_BUCKET"] ||= Litestream.replica_bucket
128
+ ENV["LITESTREAM_REPLICA_REGION"] ||= Litestream.replica_region
129
+ ENV["LITESTREAM_REPLICA_ENDPOINT"] ||= Litestream.replica_endpoint
122
130
  ENV["LITESTREAM_ACCESS_KEY_ID"] ||= Litestream.replica_key_id
123
131
  ENV["LITESTREAM_SECRET_ACCESS_KEY"] ||= Litestream.replica_access_key
124
132
 
125
133
  args = {
126
- "--config" => Rails.root.join("config", "litestream.yml").to_s
134
+ "--config" => Litestream.config_path.to_s
127
135
  }.merge(argv.stringify_keys).to_a.flatten.compact
128
136
  cmd = [executable, command, *args, database].compact
129
137
  puts cmd.inspect if ENV["DEBUG"]
@@ -2,7 +2,7 @@
2
2
  # All configuration options will be available as environment variables, e.g.
3
3
  # config.replica_bucket becomes LITESTREAM_REPLICA_BUCKET
4
4
  # This allows you to configure Litestream using Rails encrypted credentials,
5
- # or some other mechanism where the values are only avaialble at runtime.
5
+ # or some other mechanism where the values are only available at runtime.
6
6
 
7
7
  Rails.application.configure do
8
8
  # An example of using Rails encrypted credentials to configure Litestream.
@@ -4,6 +4,7 @@ module Litestream
4
4
 
5
5
  # rubygems platform name => upstream release filename
6
6
  NATIVE_PLATFORMS = {
7
+ "aarch64-linux" => "litestream-#{VERSION}-linux-arm64.tar.gz",
7
8
  "arm64-darwin" => "litestream-#{VERSION}-darwin-arm64.zip",
8
9
  "arm64-linux" => "litestream-#{VERSION}-linux-arm64.tar.gz",
9
10
  "x86_64-darwin" => "litestream-#{VERSION}-darwin-amd64.zip",
@@ -1,3 +1,3 @@
1
1
  module Litestream
2
- VERSION = "0.11.2"
2
+ VERSION = "0.13.0"
3
3
  end
data/lib/litestream.rb CHANGED
@@ -19,7 +19,7 @@ module Litestream
19
19
 
20
20
  def self.configure
21
21
  deprecator.warn(
22
- 'Configuring Litestream via Litestream.configure is deprecated. Use Rails.application.configure { config.litestream.* = ... } instead.',
22
+ "Configuring Litestream via Litestream.configure is deprecated. Use Rails.application.configure { config.litestream.* = ... } instead.",
23
23
  caller
24
24
  )
25
25
  self.configuration ||= Configuration.new
@@ -33,16 +33,18 @@ module Litestream
33
33
  end
34
34
  end
35
35
 
36
- mattr_writer :username, :password, :queue, :replica_bucket, :replica_key_id, :replica_access_key
36
+ mattr_writer :username, :password, :queue, :replica_bucket, :replica_region, :replica_endpoint, :replica_key_id, :replica_access_key, :systemctl_command,
37
+ :config_path
38
+ mattr_accessor :base_controller_class, default: "::ApplicationController"
37
39
 
38
40
  class << self
39
- def verify!(database_path)
41
+ def verify!(database_path, replication_sleep: 10)
40
42
  database = SQLite3::Database.new(database_path)
41
43
  database.execute("CREATE TABLE IF NOT EXISTS _litestream_verification (id INTEGER PRIMARY KEY, uuid BLOB)")
42
44
  sentinel = SecureRandom.uuid
43
45
  database.execute("INSERT INTO _litestream_verification (uuid) VALUES (?)", [sentinel])
44
46
  # give the Litestream replication process time to replicate the sentinel value
45
- sleep 10
47
+ sleep replication_sleep
46
48
 
47
49
  backup_path = "tmp/#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_#{sentinel}.sqlite3"
48
50
  Litestream::Commands.restore(database_path, **{"-o" => backup_path})
@@ -77,6 +79,14 @@ module Litestream
77
79
  @@replica_bucket || configuration.replica_bucket
78
80
  end
79
81
 
82
+ def replica_region
83
+ @@replica_region
84
+ end
85
+
86
+ def replica_endpoint
87
+ @@replica_endpoint
88
+ end
89
+
80
90
  def replica_key_id
81
91
  @@replica_key_id || configuration.replica_key_id
82
92
  end
@@ -85,55 +95,16 @@ module Litestream
85
95
  @@replica_access_key || configuration.replica_access_key
86
96
  end
87
97
 
98
+ def systemctl_command
99
+ @@systemctl_command || "systemctl status litestream"
100
+ end
101
+
102
+ def config_path
103
+ @@config_path || Rails.root.join("config", "litestream.yml")
104
+ end
105
+
88
106
  def replicate_process
89
- info = {}
90
- if !`which systemctl`.empty?
91
- systemctl_status = `systemctl status litestream`.chomp
92
- # ["● litestream.service - Litestream",
93
- # " Loaded: loaded (/lib/systemd/system/litestream.service; enabled; vendor preset: enabled)",
94
- # " Active: active (running) since Tue 2023-07-25 13:49:43 UTC; 8 months 24 days ago",
95
- # " Main PID: 1179656 (litestream)",
96
- # " Tasks: 9 (limit: 1115)",
97
- # " Memory: 22.9M",
98
- # " CPU: 10h 49.843s",
99
- # " CGroup: /system.slice/litestream.service",
100
- # " └─1179656 /usr/bin/litestream replicate",
101
- # "",
102
- # "Warning: some journal files were not opened due to insufficient permissions."]
103
- systemctl_status.split("\n").each do |line|
104
- line.strip!
105
- if line.start_with?("Main PID:")
106
- _key, value = line.split(":")
107
- pid, _name = value.strip.split(" ")
108
- info[:pid] = pid
109
- elsif line.start_with?("Active:")
110
- value, _ago = line.split(";")
111
- status, timestamp = value.split(" since ")
112
- info[:started] = DateTime.strptime(timestamp.strip, "%a %Y-%m-%d %H:%M:%S %Z")
113
- status_match = status.match(%r{\((?<status>.*)\)})
114
- info[:status] = status_match ? status_match[:status] : nil
115
- end
116
- end
117
- else
118
- litestream_replicate_ps = `ps -a | grep litestream | grep replicate`.chomp
119
- litestream_replicate_ps.split("\n").each do |line|
120
- next unless line.include?("litestream replicate")
121
- pid, * = line.split(" ")
122
- info[:pid] = pid
123
- state, _, lstart = `ps -o "state,lstart" #{pid}`.chomp.split("\n").last.partition(/\s+/)
124
-
125
- info[:status] = case state[0]
126
- when "I" then "idle"
127
- when "R" then "running"
128
- when "S" then "sleeping"
129
- when "T" then "stopped"
130
- when "U" then "uninterruptible"
131
- when "Z" then "zombie"
132
- end
133
- info[:started] = DateTime.strptime(lstart.strip, "%a %b %d %H:%M:%S %Y")
134
- end
135
- end
136
- info
107
+ systemctl_info || process_info || {}
137
108
  end
138
109
 
139
110
  def databases
@@ -153,6 +124,71 @@ module Litestream
153
124
  end
154
125
  end
155
126
  end
127
+
128
+ private
129
+
130
+ def systemctl_info
131
+ return if `which systemctl`.empty?
132
+
133
+ systemctl_output = `#{Litestream.systemctl_command}`
134
+ systemctl_exit_code = $?.exitstatus
135
+ return unless systemctl_exit_code.zero?
136
+
137
+ # ["● litestream.service - Litestream",
138
+ # " Loaded: loaded (/lib/systemd/system/litestream.service; enabled; vendor preset: enabled)",
139
+ # " Active: active (running) since Tue 2023-07-25 13:49:43 UTC; 8 months 24 days ago",
140
+ # " Main PID: 1179656 (litestream)",
141
+ # " Tasks: 9 (limit: 1115)",
142
+ # " Memory: 22.9M",
143
+ # " CPU: 10h 49.843s",
144
+ # " CGroup: /system.slice/litestream.service",
145
+ # " └─1179656 /usr/bin/litestream replicate",
146
+ # "",
147
+ # "Warning: some journal files were not opened due to insufficient permissions."]
148
+
149
+ info = {}
150
+ systemctl_output.chomp.split("\n").each do |line|
151
+ line.strip!
152
+ if line.start_with?("Main PID:")
153
+ _key, value = line.split(":")
154
+ pid, _name = value.strip.split(" ")
155
+ info[:pid] = pid
156
+ elsif line.start_with?("Active:")
157
+ value, _ago = line.split(";")
158
+ status, timestamp = value.split(" since ")
159
+ info[:started] = DateTime.strptime(timestamp.strip, "%a %Y-%m-%d %H:%M:%S %Z")
160
+ status_match = status.match(%r{\((?<status>.*)\)})
161
+ info[:status] = status_match ? status_match[:status] : nil
162
+ end
163
+ end
164
+ info
165
+ end
166
+
167
+ def process_info
168
+ litestream_replicate_ps = `ps -ax | grep litestream | grep replicate`
169
+ exit_code = $?.exitstatus
170
+ return unless exit_code.zero?
171
+
172
+ info = {}
173
+ litestream_replicate_ps.chomp.split("\n").each do |line|
174
+ next unless line.include?("litestream replicate")
175
+
176
+ pid, * = line.split(" ")
177
+ info[:pid] = pid
178
+ state, _, lstart = `ps -o "state,lstart" #{pid}`.chomp.split("\n").last.partition(/\s+/)
179
+
180
+ info[:status] = case state[0]
181
+ when "I" then "idle"
182
+ when "R" then "running"
183
+ when "S" then "sleeping"
184
+ when "T" then "stopped"
185
+ when "U" then "uninterruptible"
186
+ when "Z" then "zombie"
187
+ end
188
+ info[:started] = DateTime.strptime(lstart.strip, "%a %b %d %H:%M:%S %Y")
189
+ end
190
+ info
191
+ end
156
192
  end
157
193
  end
158
194
 
@@ -2,6 +2,8 @@ namespace :litestream do
2
2
  desc "Print the ENV variables needed for the Litestream config file"
3
3
  task env: :environment do
4
4
  puts "LITESTREAM_REPLICA_BUCKET=#{Litestream.replica_bucket}"
5
+ puts "LITESTREAM_REPLICA_REGION=#{Litestream.replica_region}"
6
+ puts "LITESTREAM_REPLICA_ENDPOINT=#{Litestream.replica_endpoint}"
5
7
  puts "LITESTREAM_ACCESS_KEY_ID=#{Litestream.replica_key_id}"
6
8
  puts "LITESTREAM_SECRET_ACCESS_KEY=#{Litestream.replica_access_key}"
7
9
 
@@ -75,4 +77,18 @@ namespace :litestream do
75
77
 
76
78
  Litestream::Commands.snapshots(database, async: true, **options)
77
79
  end
80
+
81
+ desc "List all wal files for a database or replica, for example `rake litestream:wal -- -database=storage/production.sqlite3`"
82
+ task wal: :environment do
83
+ options = {}
84
+ if (separator_index = ARGV.index("--"))
85
+ ARGV.slice(separator_index + 1, ARGV.length)
86
+ .map { |pair| pair.split("=") }
87
+ .each { |opt| options[opt[0]] = opt[1] || nil }
88
+ end
89
+ database = options.delete("--database") || options.delete("-database")
90
+ options.symbolize_keys!
91
+
92
+ Litestream::Commands.wal(database, async: true, **options)
93
+ end
78
94
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: litestream
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.2
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-09-06 00:00:00.000000000 Z
10
+ date: 2025-06-03 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: logfmt
@@ -150,7 +149,6 @@ dependencies:
150
149
  - - ">="
151
150
  - !ruby/object:Gem::Version
152
151
  version: '0'
153
- description:
154
152
  email:
155
153
  - stephen.margheim@gmail.com
156
154
  executables:
@@ -188,7 +186,6 @@ metadata:
188
186
  rubygems_mfa_required: 'true'
189
187
  source_code_uri: https://github.com/fractaledmind/litestream-ruby
190
188
  changelog_uri: https://github.com/fractaledmind/litestream-ruby/CHANGELOG.md
191
- post_install_message:
192
189
  rdoc_options: []
193
190
  require_paths:
194
191
  - lib
@@ -203,8 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
203
200
  - !ruby/object:Gem::Version
204
201
  version: '0'
205
202
  requirements: []
206
- rubygems_version: 3.5.11
207
- signing_key:
203
+ rubygems_version: 3.6.3
208
204
  specification_version: 4
209
205
  summary: Integrate Litestream with the RubyGems infrastructure.
210
206
  test_files: []