litestream 0.5.0 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8da245a396bc819cf0aa9bfae9a87f65a92047fd45a96bd8f8602eee6248adb6
4
- data.tar.gz: 4bee567fab0ad7ab71d72c36091be4a32c4685ddfe1b69f6403c36ea927af437
3
+ metadata.gz: 4b38d63aa3c23ba58c57dd91f178e069e898519f06b38e7ece5d0f9b9238f20d
4
+ data.tar.gz: 730e40b5ba2fd39a726902052333c049943adebbde252d2a769013585172d789
5
5
  SHA512:
6
- metadata.gz: 5b1ba5a3de6be15243513f10f37657c940cbd59471ae008434c73aa0a09b62869b7e940388003a254431ab503fafdc891f7cffcc95afcfa61c07e1f657617225
7
- data.tar.gz: 34410a43ee4ab617e160a76989d4721d7c6b1861aa15d623602a7088d1b561266e40f2243e7c0a1d880778031d15b9ce71384ae1cf4a245494f21d2ea93426fc
6
+ metadata.gz: b07f6c0cd590a5ec095f778a1b473a6168adc5e747aadfbef2ce8dbea4047ef27ed9861ce0c36ac4d78bcdf7ea819a4e2d57db4c989ee8da9190523a77ae86cf
7
+ data.tar.gz: 0f9436ff88c9adf3b521c853e3a8e770b8aea4590fd2d8eb2cb575d48dfd7044a6156982b2e2924724c1bd5a5b97840eba567b143084abaf28786a4e50523135
data/README.md CHANGED
@@ -165,6 +165,81 @@ You can forward arguments in whatever order you like, you simply need to ensure
165
165
  Disables environment variable expansion in configuration file.
166
166
  ```
167
167
 
168
+ ### Verification
169
+
170
+ You can verify the integrity of your backed-up databases using the gem's provided `litestream:verify` rake task. This rake task requires that you specify which specific database you want to verify. As with the `litestream:restore` tasks, you pass arguments to the rake task via argument forwarding. For example, to verify the production database, you would run:
171
+
172
+ ```shell
173
+ bin/rails litestream:verify -- --database=storage/production.sqlite3
174
+ # or
175
+ bundle exec rake litestream:verify -- --database=storage/production.sqlite3
176
+ ```
177
+
178
+ The `litestream:verify` rake task takes the same options as the `litestream:restore` rake task. After restoring the backup, the rake task will verify the integrity of the restored database by ensuring that the restored database file
179
+
180
+ 1. exists,
181
+ 2. can be opened by SQLite, and
182
+ 3. sufficiently matches the original database file.
183
+
184
+ Since point 3 is subjective, the rake task will output a message providing both the file size and number of tables of both the "original" and "restored" databases. You must manually verify that the restored database is within an acceptable range of the original database.
185
+
186
+ The rake task will output a message similar to the following:
187
+
188
+ ```
189
+ size
190
+ original 21688320
191
+ restored 21688320
192
+ delta 0
193
+
194
+ tables
195
+ original 9
196
+ restored 9
197
+ delta 0
198
+ ```
199
+
200
+ After restoring the backup, the `litestream:verify` rake task will delete the restored database file. If you need the restored database file, use the `litestream:restore` rake task instead.
201
+
202
+ ### Introspection
203
+
204
+ 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:
205
+
206
+ ```shell
207
+ bin/rails litestream:databases
208
+ ```
209
+
210
+ This will return a list of databases and their configured replicas:
211
+
212
+ ```
213
+ path replicas
214
+ /Users/you/Code/your-app/storage/production.sqlite3 s3
215
+ ```
216
+
217
+ You can also list the generations of a specific database:
218
+
219
+ ```shell
220
+ bin/rails litestream:generations -- --database=storage/production.sqlite3
221
+ ```
222
+
223
+ This will list all generations for the specified database, including stats about their lag behind the primary database and the time range they cover.
224
+
225
+ ```
226
+ name generation lag start end
227
+ s3 a295b16a796689f3 -156ms 2024-04-17T00:01:19Z 2024-04-17T00:01:19Z
228
+ ```
229
+
230
+ Finally, you can list the snapshots available for a database:
231
+
232
+ ```shell
233
+ bin/rails litestream:snapshots -- --database=storage/production.sqlite3
234
+ ```
235
+
236
+ This command lists snapshots available for that specified database:
237
+
238
+ ```
239
+ replica generation index size created
240
+ s3 a295b16a796689f3 1 4645465 2024-04-17T00:01:19Z
241
+ ```
242
+
168
243
  ### Additional commands
169
244
 
170
245
  The rake tasks are the recommended way to interact with the Litestream utility in your Rails application or Ruby project. But, you _can_ work directly with the Litestream CLI. Since the gem installs the native executable via Bundler, the `litestream` command will be available in your `PATH`.
@@ -14,106 +14,148 @@ module Litestream
14
14
  # raised when LITESTREAM_INSTALL_DIR does not exist
15
15
  DirectoryNotFoundException = Class.new(StandardError)
16
16
 
17
- def self.platform
18
- [:cpu, :os].map { |m| Gem::Platform.local.send(m) }.join("-")
19
- end
17
+ # raised when a litestream command requires a database argument but it isn't provided
18
+ DatabaseRequiredException = Class.new(StandardError)
19
+
20
+ # raised when litestream fails to restore a database backup
21
+ BackupFailedException = Class.new(StandardError)
22
+
23
+ class << self
24
+ def platform
25
+ [:cpu, :os].map { |m| Gem::Platform.local.send(m) }.join("-")
26
+ end
20
27
 
21
- def self.executable(exe_path: DEFAULT_DIR)
22
- litestream_install_dir = ENV["LITESTREAM_INSTALL_DIR"]
23
- if litestream_install_dir
24
- if File.directory?(litestream_install_dir)
25
- warn "NOTE: using LITESTREAM_INSTALL_DIR to find litestream executable: #{litestream_install_dir}"
26
- exe_path = litestream_install_dir
27
- exe_file = File.expand_path(File.join(litestream_install_dir, "litestream"))
28
+ def executable(exe_path: DEFAULT_DIR)
29
+ litestream_install_dir = ENV["LITESTREAM_INSTALL_DIR"]
30
+ if litestream_install_dir
31
+ if File.directory?(litestream_install_dir)
32
+ warn "NOTE: using LITESTREAM_INSTALL_DIR to find litestream executable: #{litestream_install_dir}"
33
+ exe_path = litestream_install_dir
34
+ exe_file = File.expand_path(File.join(litestream_install_dir, "litestream"))
35
+ else
36
+ raise DirectoryNotFoundException, <<~MESSAGE
37
+ LITESTREAM_INSTALL_DIR is set to #{litestream_install_dir}, but that directory does not exist.
38
+ MESSAGE
39
+ end
28
40
  else
29
- raise DirectoryNotFoundException, <<~MESSAGE
30
- LITESTREAM_INSTALL_DIR is set to #{litestream_install_dir}, but that directory does not exist.
31
- MESSAGE
41
+ if Litestream::Upstream::NATIVE_PLATFORMS.keys.none? { |p| Gem::Platform.match_gem?(Gem::Platform.new(p), GEM_NAME) }
42
+ raise UnsupportedPlatformException, <<~MESSAGE
43
+ litestream-ruby does not support the #{platform} platform
44
+ Please install litestream following instructions at https://litestream.io/install
45
+ MESSAGE
46
+ end
47
+
48
+ exe_file = Dir.glob(File.expand_path(File.join(exe_path, "*", "litestream"))).find do |f|
49
+ Gem::Platform.match_gem?(Gem::Platform.new(File.basename(File.dirname(f))), GEM_NAME)
50
+ end
32
51
  end
33
- else
34
- if Litestream::Upstream::NATIVE_PLATFORMS.keys.none? { |p| Gem::Platform.match_gem?(Gem::Platform.new(p), GEM_NAME) }
35
- raise UnsupportedPlatformException, <<~MESSAGE
36
- litestream-ruby does not support the #{platform} platform
37
- Please install litestream following instructions at https://litestream.io/install
52
+
53
+ if exe_file.nil? || !File.exist?(exe_file)
54
+ raise ExecutableNotFoundException, <<~MESSAGE
55
+ Cannot find the litestream executable for #{platform} in #{exe_path}
56
+
57
+ If you're using bundler, please make sure you're on the latest bundler version:
58
+
59
+ gem install bundler
60
+ bundle update --bundler
61
+
62
+ Then make sure your lock file includes this platform by running:
63
+
64
+ bundle lock --add-platform #{platform}
65
+ bundle install
66
+
67
+ See `bundle lock --help` output for details.
68
+
69
+ If you're still seeing this message after taking those steps, try running
70
+ `bundle config` and ensure `force_ruby_platform` isn't set to `true`. See
71
+ https://github.com/fractaledmind/litestream-ruby#check-bundle_force_ruby_platform
72
+ for more details.
38
73
  MESSAGE
39
74
  end
40
75
 
41
- exe_file = Dir.glob(File.expand_path(File.join(exe_path, "*", "litestream"))).find do |f|
42
- Gem::Platform.match_gem?(Gem::Platform.new(File.basename(File.dirname(f))), GEM_NAME)
43
- end
76
+ exe_file
44
77
  end
45
78
 
46
- if exe_file.nil? || !File.exist?(exe_file)
47
- raise ExecutableNotFoundException, <<~MESSAGE
48
- Cannot find the litestream executable for #{platform} in #{exe_path}
79
+ def replicate(async: true, **argv)
80
+ execute("replicate", argv, async: async)
81
+ end
49
82
 
50
- If you're using bundler, please make sure you're on the latest bundler version:
83
+ def restore(database, async: true, **argv)
84
+ raise DatabaseRequiredException, "database argument is required for restore command, e.g. litestream:restore -- --database=path/to/database.sqlite" if database.nil?
51
85
 
52
- gem install bundler
53
- bundle update --bundler
86
+ dir, file = File.split(database)
87
+ ext = File.extname(file)
88
+ base = File.basename(file, ext)
89
+ now = Time.now.utc.strftime("%Y%m%d%H%M%S")
90
+ backup = File.join(dir, "#{base}-#{now}#{ext}")
54
91
 
55
- Then make sure your lock file includes this platform by running:
92
+ args = {
93
+ "-o" => backup
94
+ }.merge(argv)
56
95
 
57
- bundle lock --add-platform #{platform}
58
- bundle install
96
+ execute("restore", args, database, async: async)
59
97
 
60
- See `bundle lock --help` output for details.
98
+ backup
99
+ end
61
100
 
62
- If you're still seeing this message after taking those steps, try running
63
- `bundle config` and ensure `force_ruby_platform` isn't set to `true`. See
64
- https://github.com/fractaledmind/litestream-ruby#check-bundle_force_ruby_platform
65
- for more details.
66
- MESSAGE
101
+ def databases(async: true, **argv)
102
+ execute("databases", argv, async: async)
67
103
  end
68
104
 
69
- exe_file
70
- end
105
+ def generations(database, async: true, **argv)
106
+ raise DatabaseRequiredException, "database argument is required for generations command, e.g. litestream:generations -- --database=path/to/database.sqlite" if database.nil?
71
107
 
72
- def self.replicate(argv = {})
73
- if Litestream.configuration
74
- ENV["LITESTREAM_REPLICA_BUCKET"] ||= Litestream.configuration.replica_bucket
75
- ENV["LITESTREAM_ACCESS_KEY_ID"] ||= Litestream.configuration.replica_key_id
76
- ENV["LITESTREAM_SECRET_ACCESS_KEY"] ||= Litestream.configuration.replica_access_key
108
+ execute("generations", argv, database, async: async)
77
109
  end
78
110
 
79
- args = {
80
- "--config" => Rails.root.join("config", "litestream.yml").to_s
81
- }.merge(argv).to_a.flatten.compact
82
-
83
- command = [executable, "replicate", *args]
84
- puts command.inspect
111
+ def snapshots(database, async: true, **argv)
112
+ raise DatabaseRequiredException, "database argument is required for snapshots command, e.g. litestream:snapshots -- --database=path/to/database.sqlite" if database.nil?
85
113
 
86
- # To release the resources of the Ruby process, just fork and exit.
87
- # The forked process executes litestream and replaces itself.
88
- if fork.nil?
89
- exec(*command)
114
+ execute("snapshots", argv, database, async: async)
90
115
  end
91
- end
92
116
 
93
- def self.restore(database, argv = {})
94
- if Litestream.configuration
95
- ENV["LITESTREAM_REPLICA_BUCKET"] ||= Litestream.configuration.replica_bucket
96
- ENV["LITESTREAM_ACCESS_KEY_ID"] ||= Litestream.configuration.replica_key_id
97
- ENV["LITESTREAM_SECRET_ACCESS_KEY"] ||= Litestream.configuration.replica_access_key
117
+ def verify(database, async: true, **argv)
118
+ raise DatabaseRequiredException, "database argument is required for verify command, e.g. litestream:verify -- --database=path/to/database.sqlite" if database.nil? || !File.exist?(database)
119
+
120
+ backup = restore(database, async: false, **argv)
121
+
122
+ raise BackupFailedException, "Failed to create backup for validation" unless File.exist?(backup)
123
+
124
+ restored_tables_count = `sqlite3 #{backup} "select count(*) from sqlite_schema where type='table';"`.chomp.to_i
125
+ restored_size = File.size(backup)
126
+ original_tables_count = `sqlite3 #{database} "select count(*) from sqlite_schema where type='table';"`.chomp.to_i
127
+ original_size = File.size(database)
128
+
129
+ Dir.glob(backup + "*").each { |file| File.delete(file) }
130
+
131
+ {
132
+ size: {original: original_size, restored: restored_size},
133
+ tables: {original: original_tables_count, restored: restored_tables_count}
134
+ }
98
135
  end
99
136
 
100
- dir, file = File.split(database)
101
- ext = File.extname(file)
102
- base = File.basename(file, ext)
103
- now = Time.now.utc.strftime("%Y%m%d%H%M%S")
137
+ private
104
138
 
105
- args = {
106
- "--config" => Rails.root.join("config", "litestream.yml").to_s,
107
- "-o" => File.join(dir, "#{base}-#{now}#{ext}")
108
- }.merge(argv).to_a.flatten.compact
139
+ def execute(command, argv = {}, database = nil, async: true)
140
+ if Litestream.configuration
141
+ ENV["LITESTREAM_REPLICA_BUCKET"] ||= Litestream.configuration.replica_bucket
142
+ ENV["LITESTREAM_ACCESS_KEY_ID"] ||= Litestream.configuration.replica_key_id
143
+ ENV["LITESTREAM_SECRET_ACCESS_KEY"] ||= Litestream.configuration.replica_access_key
144
+ end
109
145
 
110
- command = [executable, "restore", *args, database]
111
- puts command.inspect
146
+ args = {
147
+ "--config" => Rails.root.join("config", "litestream.yml").to_s
148
+ }.merge(argv).to_a.flatten.compact
149
+ cmd = [executable, command, *args, database].compact
150
+ puts cmd.inspect if ENV["DEBUG"]
112
151
 
113
- # To release the resources of the Ruby process, just fork and exit.
114
- # The forked process executes litestream and replaces itself.
115
- if fork.nil?
116
- exec(*command)
152
+ if async
153
+ # To release the resources of the Ruby process, just fork and exit.
154
+ # The forked process executes litestream and replaces itself.
155
+ exec(*cmd) if fork.nil?
156
+ else
157
+ system(*cmd)
158
+ end
117
159
  end
118
160
  end
119
161
  end
@@ -1,3 +1,3 @@
1
1
  module Litestream
2
- VERSION = "0.5.0"
2
+ VERSION = "0.5.2"
3
3
  end
@@ -36,4 +36,65 @@ namespace :litestream do
36
36
 
37
37
  Litestream::Commands.restore(options.delete("--database") || options.delete("-database"), options)
38
38
  end
39
+
40
+ desc "List all databases and associated replicas in the config file, e.g. rake litestream:databases -- -no-expand-env"
41
+ task databases: :environment do
42
+ options = {}
43
+ if (separator_index = ARGV.index("--"))
44
+ ARGV.slice(separator_index + 1, ARGV.length)
45
+ .map { |pair| pair.split("=") }
46
+ .each { |opt| options[opt[0]] = opt[1] || nil }
47
+ end
48
+
49
+ Litestream::Commands.databases(options)
50
+ end
51
+
52
+ desc "List all generations for a database or replica, e.g. rake litestream:generations -- -database=storage/production.sqlite3"
53
+ task generations: :environment do
54
+ options = {}
55
+ if (separator_index = ARGV.index("--"))
56
+ ARGV.slice(separator_index + 1, ARGV.length)
57
+ .map { |pair| pair.split("=") }
58
+ .each { |opt| options[opt[0]] = opt[1] || nil }
59
+ end
60
+
61
+ Litestream::Commands.generations(options.delete("--database") || options.delete("-database"), options)
62
+ end
63
+
64
+ desc "List all snapshots for a database or replica, e.g. rake litestream:snapshots -- -database=storage/production.sqlite3"
65
+ task snapshots: :environment do
66
+ options = {}
67
+ if (separator_index = ARGV.index("--"))
68
+ ARGV.slice(separator_index + 1, ARGV.length)
69
+ .map { |pair| pair.split("=") }
70
+ .each { |opt| options[opt[0]] = opt[1] || nil }
71
+ end
72
+
73
+ Litestream::Commands.snapshots(options.delete("--database") || options.delete("-database"), options)
74
+ end
75
+
76
+ desc "verify backup of SQLite database from a Litestream replica, e.g. rake litestream:verify -- -database=storage/production.sqlite3"
77
+ task verify: :environment do
78
+ options = {}
79
+ if (separator_index = ARGV.index("--"))
80
+ ARGV.slice(separator_index + 1, ARGV.length)
81
+ .map { |pair| pair.split("=") }
82
+ .each { |opt| options[opt[0]] = opt[1] || nil }
83
+ end
84
+
85
+ result = Litestream::Commands.verify(options.delete("--database") || options.delete("-database"), options)
86
+
87
+ puts <<~TXT if result
88
+
89
+ size
90
+ original #{result[:size][:original]}
91
+ restored #{result[:size][:restored]}
92
+ delta #{result[:size][:original] - result[:size][:restored]}
93
+
94
+ tables
95
+ original #{result[:tables][:original]}
96
+ restored #{result[:tables][:restored]}
97
+ delta #{result[:tables][:original] - result[:tables][:restored]}
98
+ TXT
99
+ end
39
100
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: litestream
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim