├── test ├── dummy │ ├── log │ │ ├── .keep │ │ └── development.log │ ├── tmp │ │ ├── .keep │ │ ├── pids │ │ │ └── .keep │ │ ├── storage │ │ │ └── .keep │ │ └── local_secret.txt │ ├── storage │ │ ├── .keep │ │ └── test.sqlite3 │ ├── app │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ └── my_application_controller.rb │ │ ├── models │ │ │ └── application_record.rb │ │ └── views │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── bin │ │ ├── rake │ │ ├── rails │ │ └── setup │ ├── config │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── routes.rb │ │ ├── litestream.yml │ │ ├── locales │ │ │ └── en.yml │ │ ├── database.yml │ │ ├── application.rb │ │ ├── puma.rb │ │ └── environments │ │ │ └── test.rb │ └── config.ru ├── test_helper.rb ├── litestream │ ├── test_base_application_controller.rb │ └── test_commands.rb ├── controllers │ └── test_processes_controller.rb ├── generators │ └── test_install.rb ├── test_litestream.rb └── tasks │ └── test_litestream_tasks.rb ├── lib ├── litestream │ ├── version.rb │ ├── upstream.rb │ ├── engine.rb │ ├── generators │ │ └── litestream │ │ │ ├── install_generator.rb │ │ │ └── templates │ │ │ ├── config.yml.erb │ │ │ └── initializer.rb │ └── commands.rb ├── puma │ └── plugin │ │ └── litestream.rb ├── tasks │ └── litestream_tasks.rake └── litestream.rb ├── images └── show-screenshot.png ├── .standard.yml ├── sig └── litestream.rbs ├── bin ├── setup ├── console └── release ├── config └── routes.rb ├── .gitignore ├── Gemfile ├── app ├── controllers │ └── litestream │ │ ├── processes_controller.rb │ │ ├── application_controller.rb │ │ └── restorations_controller.rb ├── jobs │ └── litestream │ │ └── verification_job.rb └── views │ ├── layouts │ └── litestream │ │ ├── application.html.erb │ │ └── _style.html │ └── litestream │ └── processes │ └── show.html.erb ├── Rakefile ├── exe └── litestream ├── .github └── workflows │ ├── main.yml │ └── gem-install.yml ├── LICENSE ├── litestream.gemspec ├── rakelib └── package.rake ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── Gemfile.lock ├── LICENSE-DEPENDENCIES └── README.md /test/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/tmp/pids/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/tmp/storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/litestream/version.rb: -------------------------------------------------------------------------------- 1 | module Litestream 2 | VERSION = "0.14.0" 3 | end 4 | -------------------------------------------------------------------------------- /images/show-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/litestream-ruby/HEAD/images/show-screenshot.png -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/my_application_controller.rb: -------------------------------------------------------------------------------- 1 | class MyApplicationController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/storage/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/litestream-ruby/HEAD/test/dummy/storage/test.sqlite3 -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | # For available configuration options, see: 2 | # https://github.com/testdouble/standard 3 | ruby_version: 3.0 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /sig/litestream.rbs: -------------------------------------------------------------------------------- 1 | module Litestream 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/tmp/local_secret.txt: -------------------------------------------------------------------------------- 1 | 2e674226836fd5b8d265fbc2088757c026b86086afb1ea213271ec09b432cce56122da2e7ba0ab5e70ab06f5b1b07fdf386b0cea6b28efce5afaa11d5aa1e76f -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 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 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /exe/*/litestream 10 | test/dummy/log/test.log 11 | .DS_Store 12 | /test/**/*.sqlite3 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in litestream.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | 10 | gem "minitest", "~> 5.0" 11 | 12 | gem "standard", "~> 1.3" 13 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) 3 | 4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) 6 | -------------------------------------------------------------------------------- /app/controllers/litestream/processes_controller.rb: -------------------------------------------------------------------------------- 1 | module Litestream 2 | class ProcessesController < ApplicationController 3 | # GET /process 4 | def show 5 | @process = Litestream.replicate_process 6 | @databases = Litestream.databases 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/dummy/log/development.log: -------------------------------------------------------------------------------- 1 | [ActionDispatch::HostAuthorization::DefaultResponseApp] Blocked hosts: www.example.com 2 | [ActionDispatch::HostAuthorization::DefaultResponseApp] Blocked hosts: www.example.com 3 | [ActionDispatch::HostAuthorization::DefaultResponseApp] Blocked hosts: www.example.com 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << "test" 8 | t.libs << "lib" 9 | t.test_files = FileList["test/**/test_*.rb"] 10 | end 11 | 12 | require "standard/rake" 13 | 14 | task default: %i[test standard] 15 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/jobs/litestream/verification_job.rb: -------------------------------------------------------------------------------- 1 | require "active_job" 2 | 3 | module Litestream 4 | class VerificationJob < ActiveJob::Base 5 | queue_as Litestream.queue 6 | 7 | def perform 8 | Litestream::Commands.databases.each do |db_hash| 9 | Litestream.verify!(db_hash["path"]) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "rails" 6 | require "litestream" 7 | 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | 11 | require "irb" 12 | IRB.start(__FILE__) 13 | -------------------------------------------------------------------------------- /app/controllers/litestream/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Litestream 2 | class ApplicationController < Litestream.base_controller_class.constantize 3 | protect_from_forgery with: :exception 4 | 5 | if Litestream.password 6 | http_basic_authenticate_with( 7 | name: Litestream.username, 8 | password: Litestream.password 9 | ) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /exe/litestream: -------------------------------------------------------------------------------- 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 13 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["RAILS_ENV"] = "test" 4 | 5 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 6 | 7 | require_relative "../test/dummy/config/environment" 8 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] 9 | require "rails/test_help" 10 | require "litestream" 11 | 12 | require "minitest/autorun" 13 | -------------------------------------------------------------------------------- /test/litestream/test_base_application_controller.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Litestream::BaseApplicationControllerTest < ActiveSupport::TestCase 4 | test "engine's ApplicationController inherits from host's ApplicationController by default" do 5 | assert Litestream::ApplicationController < ApplicationController 6 | end 7 | 8 | test "engine's ApplicationController inherits from configured base_controller_class" do 9 | assert Litestream::ApplicationController < MyApplicationController 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 3 | mount Litestream::Engine => "/litestream" 4 | 5 | # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. 6 | # Can be used by load balancers and uptime monitors to verify that the app is live. 7 | get "up" => "rails/health#show", :as => :rails_health_check 8 | 9 | # Defines the root path route ("/") 10 | # root "posts#index" 11 | end 12 | -------------------------------------------------------------------------------- /lib/litestream/upstream.rb: -------------------------------------------------------------------------------- 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 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '3.2.1' 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - name: Run the default task 27 | run: bundle exec rake 28 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | VERSION=$1 4 | 5 | if [ -z "$VERSION" ]; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | printf "module Litestream\n VERSION = \"$VERSION\"\nend\n" > ./lib/litestream/version.rb 11 | bundle 12 | git add Gemfile.lock lib/litestream/version.rb 13 | git commit -m "Bump version for $VERSION" 14 | git push 15 | git tag v$VERSION 16 | git push --tags 17 | 18 | rake package 19 | for gem in pkg/litestream-$VERSION*.gem ; do 20 | gem push "$gem" --host https://rubygems.org 21 | if [ $? -eq 0 ]; then 22 | rm "$gem" 23 | rm -rf "${gem/.gem/}" 24 | fi 25 | done 26 | -------------------------------------------------------------------------------- /app/controllers/litestream/restorations_controller.rb: -------------------------------------------------------------------------------- 1 | module Litestream 2 | class RestorationsController < ApplicationController 3 | # POST /restorations 4 | def create 5 | database = params[:database].remove("[ROOT]/") 6 | dir, file = File.split(database) 7 | ext = File.extname(file) 8 | base = File.basename(file, ext) 9 | now = Time.now.utc.strftime("%Y%m%d%H%M%S") 10 | backup = File.join(dir, "#{base}-#{now}#{ext}") 11 | 12 | Litestream::Commands.restore(database, **{"-o" => backup}) 13 | 14 | redirect_to root_path, notice: "Restored to #{backup}." 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/litestream/engine.rb: -------------------------------------------------------------------------------- 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 27 | -------------------------------------------------------------------------------- /test/dummy/config/litestream.yml: -------------------------------------------------------------------------------- 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 | - path: storage/test.sqlite3 14 | replicas: 15 | - type: s3 16 | bucket: $LITESTREAM_REPLICA_BUCKET 17 | path: test 18 | endpoint: http://localhost:9000 19 | access-key-id: $LITESTREAM_ACCESS_KEY_ID 20 | secret-access-key: $LITESTREAM_SECRET_ACCESS_KEY 21 | -------------------------------------------------------------------------------- /lib/litestream/generators/litestream/install_generator.rb: -------------------------------------------------------------------------------- 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 30 | -------------------------------------------------------------------------------- /lib/litestream/generators/litestream/templates/config.yml.erb: -------------------------------------------------------------------------------- 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 -%> 22 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization and 2 | # are automatically loaded by Rails. If you want to use locales other than 3 | # English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more about the API, please read the Rails Internationalization guide 20 | # at https://guides.rubyonrails.org/i18n.html. 21 | # 22 | # Be aware that YAML interprets the following case-insensitive strings as 23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings 24 | # must be quoted to be interpreted as strings. For example: 25 | # 26 | # en: 27 | # "yes": yup 28 | # enabled: "ON" 29 | 30 | en: 31 | hello: "Hello world" 32 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args, exception: true) 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Stephen Margheim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | primary: &primary 13 | <<: *default 14 | database: storage/<%= ENV.fetch("RAILS_ENV", "development") %>.sqlite3 15 | 16 | queue: &queue 17 | <<: *default 18 | migrations_paths: db/queue_migrate 19 | database: storage/queue.sqlite3 20 | 21 | errors: &errors 22 | <<: *default 23 | migrations_paths: db/errors_migrate 24 | database: storage/errors.sqlite3 25 | 26 | development: 27 | primary: 28 | <<: *primary 29 | database: storage/<%= `git branch --show-current`.chomp || 'development' %>.sqlite3 30 | queue: *queue 31 | errors: *errors 32 | 33 | # Warning: The database defined as "test" will be erased and 34 | # re-generated from your development database when you run "rake". 35 | # Do not set this db to the same as development or production. 36 | test: 37 | primary: 38 | <<: *primary 39 | database: db/test.sqlite3 40 | queue: 41 | <<: *queue 42 | database: db/queue.sqlite3 43 | errors: 44 | <<: *errors 45 | database: db/errors.sqlite3 46 | 47 | production: 48 | primary: *primary 49 | queue: *queue 50 | errors: *errors 51 | -------------------------------------------------------------------------------- /test/controllers/test_processes_controller.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Litestream::TestProcessesController < ActionDispatch::IntegrationTest 4 | test "should show the process" do 5 | stubbed_process = {pid: "12345", status: "sleeping", started: DateTime.now} 6 | stubbed_databases = [ 7 | {"path" => "[ROOT]/storage/test.sqlite3", 8 | "replicas" => "s3", 9 | "generations" => [ 10 | {"generation" => SecureRandom.hex, 11 | "name" => "s3", 12 | "lag" => "23h59m59s", 13 | "start" => "2024-05-02T11:32:16Z", 14 | "end" => "2024-05-02T11:33:10Z", 15 | "snapshots" => [ 16 | {"index" => "0", "size" => "4145735", "created" => "2024-05-02T11:32:16Z"} 17 | ]} 18 | ]} 19 | ] 20 | Litestream.stub :replicate_process, stubbed_process do 21 | Litestream.stub :databases, stubbed_databases do 22 | get litestream.process_url 23 | assert_response :success 24 | 25 | assert_select "#process_12345", 1 do 26 | assert_select "small", "sleeping" 27 | assert_select "code", "12345" 28 | assert_select "time", stubbed_process[:started].to_formatted_s(:db) 29 | end 30 | 31 | assert_select "#databases li", 1 do 32 | assert_select "h2 code", stubbed_databases[0]["path"] 33 | assert_select "details##{stubbed_databases[0]["generations"][0]["generation"]}" 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/generators/test_install.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "rails/generators" 5 | require "litestream/generators/litestream/install_generator" 6 | 7 | class LitestreamGeneratorTest < Rails::Generators::TestCase 8 | tests Litestream::Generators::InstallGenerator 9 | destination File.expand_path("../tmp", __dir__) 10 | 11 | setup :prepare_destination 12 | 13 | def after_teardown 14 | FileUtils.rm_rf destination_root 15 | super 16 | end 17 | 18 | test "should generate a Litestream configuration file" do 19 | run_generator 20 | 21 | assert_file "config/litestream.yml" do |content| 22 | assert_match "- path: storage/test.sqlite3", content 23 | assert_match "- path: storage/queue.sqlite3", content 24 | assert_match "- path: storage/errors.sqlite3", content 25 | assert_match "bucket: $LITESTREAM_REPLICA_BUCKET", content 26 | assert_match "access-key-id: $LITESTREAM_ACCESS_KEY_ID", content 27 | assert_match "secret-access-key: $LITESTREAM_SECRET_ACCESS_KEY", content 28 | end 29 | 30 | assert_file "config/initializers/litestream.rb" do |content| 31 | assert_match "config.litestream.replica_bucket = litestream_credentials&.replica_bucket", content 32 | assert_match "config.litestream.replica_key_id = litestream_credentials&.replica_key_id", content 33 | assert_match "config.litestream.replica_access_key = litestream_credentials&.replica_access_key", content 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /litestream.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/litestream/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "litestream" 7 | spec.version = Litestream::VERSION 8 | spec.authors = ["Stephen Margheim"] 9 | spec.email = ["stephen.margheim@gmail.com"] 10 | 11 | spec.summary = "Integrate Litestream with the RubyGems infrastructure." 12 | spec.homepage = "https://github.com/fractaledmind/litestream-ruby" 13 | spec.license = "MIT" 14 | spec.required_ruby_version = ">= 3.0.0" 15 | 16 | spec.metadata = { 17 | "homepage_uri" => spec.homepage, 18 | "rubygems_mfa_required" => "true", 19 | "source_code_uri" => spec.homepage, 20 | "changelog_uri" => "https://github.com/fractaledmind/litestream-ruby/CHANGELOG.md" 21 | } 22 | 23 | spec.files = Dir["{app,config,lib}/**/*", "LICENSE", "Rakefile", "README.md"] 24 | spec.bindir = "exe" 25 | spec.executables << "litestream" 26 | 27 | # Uncomment to register a new dependency of your gem 28 | spec.add_dependency "sqlite3" 29 | ">= 7.0".tap do |rails_version| 30 | spec.add_dependency "actionpack", rails_version 31 | spec.add_dependency "actionview", rails_version 32 | spec.add_dependency "activejob", rails_version 33 | spec.add_dependency "activesupport", rails_version 34 | spec.add_dependency "railties", rails_version 35 | end 36 | spec.add_development_dependency "rails" 37 | spec.add_development_dependency "rubyzip" 38 | 39 | # For more information and examples about making a new gem, check out our 40 | # guide at: https://bundler.io/guides/creating_gem.html 41 | end 42 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | # require "active_job/railtie" 7 | require "active_record/railtie" 8 | # require "active_storage/engine" 9 | require "action_controller/railtie" 10 | # require "action_mailer/railtie" 11 | # require "action_mailbox/engine" 12 | # require "action_text/engine" 13 | require "action_view/railtie" 14 | # require "action_cable/engine" 15 | require "rails/test_unit/railtie" 16 | 17 | # Require the gems listed in Gemfile, including any gems 18 | # you've limited to :test, :development, or :production. 19 | Bundler.require(*Rails.groups) 20 | 21 | module Dummy 22 | class Application < Rails::Application 23 | config.load_defaults Rails::VERSION::STRING.to_f 24 | 25 | # For compatibility with applications that use this config 26 | config.action_controller.include_all_helpers = false 27 | 28 | # Please, add to the `ignore` list any other `lib` subdirectories that do 29 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 30 | # Common ones are `templates`, `generators`, or `middleware`, for example. 31 | config.autoload_lib(ignore: %w[assets tasks]) 32 | 33 | # Configuration for the application, engines, and railties goes here. 34 | # 35 | # These settings can be overridden in specific environments using the files 36 | # in config/environments, which are processed later. 37 | # 38 | # config.time_zone = "Central Time (US & Canada)" 39 | # config.eager_load_paths << Rails.root.join("extras") 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | 5 | # Puma can serve each request in a thread from an internal thread pool. 6 | # The `threads` method setting takes two numbers: a minimum and maximum. 7 | # Any libraries that use thread pools should be configured to match 8 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 9 | # and maximum; this matches the default thread size of Active Record. 10 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 11 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 12 | threads min_threads_count, max_threads_count 13 | 14 | # Specifies that the worker count should equal the number of processors in production. 15 | if ENV["RAILS_ENV"] == "production" 16 | require "concurrent-ruby" 17 | worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) 18 | workers worker_count if worker_count > 1 19 | end 20 | 21 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 22 | # terminating a worker in development environments. 23 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 24 | 25 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 26 | port ENV.fetch("PORT") { 3000 } 27 | 28 | # Specifies the `environment` that Puma will run in. 29 | environment ENV.fetch("RAILS_ENV") { "development" } 30 | 31 | # Specifies the `pidfile` that Puma will use. 32 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 33 | 34 | # Allow puma to be restarted by `bin/rails restart` command. 35 | plugin :tmp_restart 36 | -------------------------------------------------------------------------------- /lib/puma/plugin/litestream.rb: -------------------------------------------------------------------------------- 1 | require "puma/plugin" 2 | 3 | # Copied from https://github.com/rails/solid_queue/blob/15408647f1780033dad223d3198761ea2e1e983e/lib/puma/plugin/solid_queue.rb 4 | Puma::Plugin.create do 5 | attr_reader :puma_pid, :litestream_pid, :log_writer 6 | 7 | def start(launcher) 8 | @log_writer = launcher.log_writer 9 | @puma_pid = $$ 10 | 11 | launcher.events.on_booted do 12 | @litestream_pid = fork do 13 | Thread.new { monitor_puma } 14 | Litestream::Commands.replicate(async: true) 15 | end 16 | 17 | in_background do 18 | monitor_litestream 19 | end 20 | end 21 | 22 | launcher.events.on_stopped { stop_litestream } 23 | launcher.events.on_restart { stop_litestream } 24 | end 25 | 26 | private 27 | 28 | def stop_litestream 29 | Process.waitpid(litestream_pid, Process::WNOHANG) 30 | log_writer.log "Stopping Litestream..." 31 | Process.kill(:INT, litestream_pid) if litestream_pid 32 | Process.wait(litestream_pid) 33 | rescue Errno::ECHILD, Errno::ESRCH 34 | end 35 | 36 | def monitor_puma 37 | monitor(:puma_dead?, "Detected Puma has gone away, stopping Litestream...") 38 | end 39 | 40 | def monitor_litestream 41 | monitor(:litestream_dead?, "Detected Litestream has gone away, stopping Puma...") 42 | end 43 | 44 | def monitor(process_dead, message) 45 | loop do 46 | if send(process_dead) 47 | log message 48 | Process.kill(:INT, $$) 49 | break 50 | end 51 | sleep 2 52 | end 53 | end 54 | 55 | def litestream_dead? 56 | Process.waitpid(litestream_pid, Process::WNOHANG) 57 | false 58 | rescue Errno::ECHILD, Errno::ESRCH 59 | true 60 | end 61 | 62 | def puma_dead? 63 | Process.ppid != puma_pid 64 | end 65 | 66 | def log(...) 67 | log_writer.log(...) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /app/views/layouts/litestream/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Litestream 5 | <%= csrf_meta_tags %> 6 | <%= csp_meta_tag %> 7 | 8 | <%= render "layouts/litestream/style" %> 9 | 10 | 11 |
12 | <%= content_for?(:content) ? yield(:content) : yield %> 13 |
14 | 15 | 21 | 22 |
23 | <% if notice.present? %> 24 |

27 | <%= notice.html_safe %> 28 |

29 | <% end %> 30 | 31 | <% if alert.present? %> 32 |

35 | <%= alert.html_safe %> 36 |

37 | <% end %> 38 |
39 | 40 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/litestream/generators/litestream/templates/initializer.rb: -------------------------------------------------------------------------------- 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 | # Configure Litestream through environment variables. Use Rails encrypted credentials for secrets. 9 | # litestream_credentials = Rails.application.credentials.litestream 10 | 11 | # Replica-specific bucket location. This will be your bucket's URL without the `https://` prefix. 12 | # For example, if you used DigitalOcean Spaces, your bucket URL could look like: 13 | # 14 | # https://myapp.fra1.digitaloceanspaces.com 15 | # 16 | # And so you should set your `replica_bucket` to: 17 | # 18 | # myapp.fra1.digitaloceanspaces.com 19 | # 20 | # config.litestream.replica_bucket = litestream_credentials&.replica_bucket 21 | # 22 | # Replica-specific authentication key. Litestream needs authentication credentials to access your storage provider bucket. 23 | # config.litestream.replica_key_id = litestream_credentials&.replica_key_id 24 | # 25 | # Replica-specific secret key. Litestream needs authentication credentials to access your storage provider bucket. 26 | # config.litestream.replica_access_key = litestream_credentials&.replica_access_key 27 | # 28 | # Replica-specific region. Set the bucket’s region. Only used for AWS S3 & Backblaze B2. 29 | # config.litestream.replica_region = "us-east-1" 30 | # 31 | # Replica-specific endpoint. Set the endpoint URL of the S3-compatible service. Only required for non-AWS services. 32 | # config.litestream.replica_endpoint = "endpoint.your-objectstorage.com" 33 | 34 | # Configure the default Litestream config path 35 | # config.config_path = Rails.root.join("config", "litestream.yml") 36 | 37 | # Configure the Litestream dashboard 38 | # 39 | # Set the default base controller class 40 | # config.litestream.base_controller_class = "MyApplicationController" 41 | # 42 | # Set authentication credentials for Litestream dashboard 43 | # config.litestream.username = litestream_credentials&.username 44 | # config.litestream.password = litestream_credentials&.password 45 | end 46 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # While tests run files are not watched, reloading is not necessary. 12 | config.enable_reloading = false 13 | 14 | # Eager loading loads your entire application. When running a single test locally, 15 | # this is usually not necessary, and can slow down your test suite. However, it's 16 | # recommended that you enable it in continuous integration systems to ensure eager 17 | # loading is working properly before deploying your code. 18 | config.eager_load = ENV["CI"].present? 19 | 20 | # Configure public file server for tests with Cache-Control for performance. 21 | config.public_file_server.enabled = true 22 | config.public_file_server.headers = { 23 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 24 | } 25 | 26 | # Show full error reports and disable caching. 27 | config.consider_all_requests_local = true 28 | config.action_controller.perform_caching = false 29 | config.cache_store = :null_store 30 | 31 | # Render exception templates for rescuable exceptions and raise for other exceptions. 32 | config.action_dispatch.show_exceptions = :rescuable 33 | 34 | # Disable request forgery protection in test environment. 35 | config.action_controller.allow_forgery_protection = false 36 | 37 | # Store uploaded files on the local file system in a temporary directory. 38 | # config.active_storage.service = :test 39 | 40 | # config.action_mailer.perform_caching = false 41 | 42 | # Tell Action Mailer not to deliver emails to the real world. 43 | # The :test delivery method accumulates sent emails in the 44 | # ActionMailer::Base.deliveries array. 45 | # config.action_mailer.delivery_method = :test 46 | 47 | # Print deprecation notices to the stderr. 48 | config.active_support.deprecation = :stderr 49 | 50 | # Raise exceptions for disallowed deprecations. 51 | config.active_support.disallowed_deprecation = :raise 52 | 53 | # Tell Active Support which deprecation messages to disallow. 54 | config.active_support.disallowed_deprecation_warnings = [] 55 | 56 | # Raises error for missing translations. 57 | # config.i18n.raise_on_missing_translations = true 58 | 59 | # Annotate rendered view with file names. 60 | # config.action_view.annotate_rendered_view_with_filenames = true 61 | 62 | # Raise error when a before_action's only/except options reference missing actions 63 | config.action_controller.raise_on_missing_callback_actions = true 64 | 65 | config.litestream.base_controller_class = "MyApplicationController" 66 | end 67 | -------------------------------------------------------------------------------- /lib/tasks/litestream_tasks.rake: -------------------------------------------------------------------------------- 1 | namespace :litestream do 2 | desc "Print the ENV variables needed for the Litestream config file" 3 | task env: :environment do 4 | puts "LITESTREAM_REPLICA_BUCKET=#{Litestream.replica_bucket}" 5 | puts "LITESTREAM_REPLICA_REGION=#{Litestream.replica_region}" 6 | puts "LITESTREAM_REPLICA_ENDPOINT=#{Litestream.replica_endpoint}" 7 | puts "LITESTREAM_ACCESS_KEY_ID=#{Litestream.replica_key_id}" 8 | puts "LITESTREAM_SECRET_ACCESS_KEY=#{Litestream.replica_access_key}" 9 | 10 | true 11 | end 12 | 13 | desc 'Monitor and continuously replicate SQLite databases defined in your config file, for example `rake litestream:replicate -- -exec "foreman start"`' 14 | task replicate: :environment do 15 | options = parse_argv_options 16 | 17 | Litestream::Commands.replicate(**options) 18 | end 19 | 20 | desc "Restore a SQLite database from a Litestream replica, for example `rake litestream:restore -- -database=storage/production.sqlite3`" 21 | task restore: :environment do 22 | options = parse_argv_options 23 | database = options.delete(:"--database") || options.delete(:"-database") 24 | 25 | puts Litestream::Commands.restore(database, **options) 26 | end 27 | 28 | desc "List all databases and associated replicas in the config file, for example `rake litestream:databases -- -no-expand-env`" 29 | task databases: :environment do 30 | options = parse_argv_options 31 | 32 | puts Litestream::Commands::Output.format(Litestream::Commands.databases(**options)) 33 | end 34 | 35 | desc "List all generations for a database or replica, for example `rake litestream:generations -- -database=storage/production.sqlite3`" 36 | task generations: :environment do 37 | options = parse_argv_options 38 | database = options.delete(:"--database") || options.delete(:"-database") 39 | 40 | puts Litestream::Commands::Output.format(Litestream::Commands.generations(database, **options)) 41 | end 42 | 43 | desc "List all snapshots for a database or replica, for example `rake litestream:snapshots -- -database=storage/production.sqlite3`" 44 | task snapshots: :environment do 45 | options = parse_argv_options 46 | database = options.delete(:"--database") || options.delete(:"-database") 47 | 48 | puts Litestream::Commands::Output.format(Litestream::Commands.snapshots(database, **options)) 49 | end 50 | 51 | desc "List all wal files for a database or replica, for example `rake litestream:wal -- -database=storage/production.sqlite3`" 52 | task wal: :environment do 53 | options = parse_argv_options 54 | database = options.delete(:"--database") || options.delete(:"-database") 55 | 56 | puts Litestream::Commands::Output.format( 57 | Litestream::Commands.wal(database, **options) 58 | ) 59 | end 60 | 61 | private 62 | 63 | def parse_argv_options 64 | options = {} 65 | if (separator_index = ARGV.index("--")) 66 | ARGV.slice(separator_index + 1, ARGV.length) 67 | .map { |pair| pair.split("=") } 68 | .each { |opt| options[opt[0]] = opt[1] || nil } 69 | end 70 | options.symbolize_keys! 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/test_litestream.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class TestLitestream < Minitest::Test 6 | def teardown 7 | Litestream.systemctl_command = nil 8 | end 9 | 10 | def test_that_it_has_a_version_number 11 | refute_nil ::Litestream::VERSION 12 | end 13 | 14 | def test_replicate_process_systemd 15 | stubbed_status = ["● litestream.service - Litestream", 16 | " Loaded: loaded (/lib/systemd/system/litestream.service; enabled; vendor preset: enabled)", 17 | " Active: active (running) since Tue 2023-07-25 13:49:43 UTC; 8 months 24 days ago", 18 | " Main PID: 1179656 (litestream)", 19 | " Tasks: 9 (limit: 1115)", 20 | " Memory: 22.9M", 21 | " CPU: 10h 49.843s", 22 | " CGroup: /system.slice/litestream.service", 23 | " └─1179656 /usr/bin/litestream replicate", 24 | "", 25 | "Warning: some journal files were not opened due to insufficient permissions."].join("\n") 26 | Litestream.stub :`, stubbed_status do 27 | info = Litestream.replicate_process 28 | 29 | assert_equal info[:status], "running" 30 | assert_equal info[:pid], "1179656" 31 | assert_equal info[:started].class, DateTime 32 | end 33 | end 34 | 35 | def test_replicate_process_systemd_custom_command 36 | stubbed_status = ["● myapp-litestream.service - Litestream", 37 | " Loaded: loaded (/lib/systemd/system/litestream.service; enabled; vendor preset: enabled)", 38 | " Active: active (running) since Tue 2023-07-25 13:49:43 UTC; 8 months 24 days ago", 39 | " Main PID: 1179656 (litestream)", 40 | " Tasks: 9 (limit: 1115)", 41 | " Memory: 22.9M", 42 | " CPU: 10h 49.843s", 43 | " CGroup: /system.slice/litestream.service", 44 | " └─1179656 /usr/bin/litestream replicate", 45 | "", 46 | "Warning: some journal files were not opened due to insufficient permissions."].join("\n") 47 | Litestream.systemctl_command = "systemctl --user status myapp-litestream.service" 48 | 49 | Litestream.stub :`, stubbed_status do 50 | info = Litestream.replicate_process 51 | 52 | assert_equal info[:status], "running" 53 | assert_equal info[:pid], "1179656" 54 | assert_equal info[:started].class, DateTime 55 | end 56 | end 57 | 58 | def test_replicate_process_ps 59 | stubbed_ps_list = [ 60 | "40358 ttys008 0:01.11 ruby --yjit bin/rails litestream:replicate", 61 | "40364 ttys008 0:00.07 /path/to/litestream-ruby/exe/architecture/litestream replicate --config /path/to/app/config/litestream.yml" 62 | ].join("\n") 63 | 64 | stubbed_ps_status = [ 65 | "STAT STARTED", 66 | "S+ Mon Jul 1 11:10:58 2024" 67 | ].join("\n") 68 | 69 | stubbed_backticks = proc do |arg| 70 | case arg 71 | when "ps -ax | grep litestream | grep replicate" 72 | stubbed_ps_list 73 | when %(ps -o "state,lstart" 40364) 74 | stubbed_ps_status 75 | else 76 | "" 77 | end 78 | end 79 | 80 | Litestream.stub :`, stubbed_backticks do 81 | info = Litestream.replicate_process 82 | 83 | assert_equal info[:status], "sleeping" 84 | assert_equal info[:pid], "40364" 85 | assert_equal info[:started].class, DateTime 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /.github/workflows/gem-install.yml: -------------------------------------------------------------------------------- 1 | name: Native Gems 2 | concurrency: 3 | group: "${{github.workflow}}-${{github.ref}}" 4 | cancel-in-progress: true 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | tags: 11 | - v*.*.* 12 | pull_request: 13 | types: [opened, synchronize] 14 | branches: 15 | - '*' 16 | 17 | jobs: 18 | package: 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | platform: ["ruby", "x86_64-darwin", "arm64-darwin", "x86_64-linux", "arm64-linux", "aarch64-linux"] 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - run: rm Gemfile.lock 27 | - uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: "3.2" 30 | bundler: latest 31 | bundler-cache: true 32 | - run: "bundle exec rake gem:${{matrix.platform}}" 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: gem-${{matrix.platform}} 36 | path: pkg 37 | retention-days: 1 38 | 39 | vanilla-install: 40 | needs: ["package"] 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: "3.2" 46 | - uses: actions/download-artifact@v4 47 | with: 48 | name: gem-ruby 49 | path: pkg 50 | - run: "gem install pkg/litestream-*.gem" 51 | - run: "litestream 2>&1 | fgrep 'ERROR: Cannot find the litestream executable'" 52 | 53 | linux-x86_64-install: 54 | needs: ["package"] 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: ruby/setup-ruby@v1 58 | with: 59 | ruby-version: "3.2" 60 | - uses: actions/download-artifact@v4 61 | with: 62 | name: gem-x86_64-linux 63 | path: pkg 64 | - run: "gem install pkg/litestream-*.gem" 65 | - run: "litestream version" 66 | 67 | # linux-arm64-install: 68 | # needs: ["package"] 69 | # runs-on: ubuntu-latest 70 | # steps: 71 | # - uses: ruby/setup-ruby@v1 72 | # with: 73 | # ruby-version: "3.2" 74 | # - uses: actions/download-artifact@v4 75 | # with: 76 | # name: gem-arm64-linux 77 | # path: pkg 78 | # - run: | 79 | # docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 80 | # docker run --rm -v "$(pwd):/test" -w /test --platform=linux/arm/v7 ruby:3.2 \ 81 | # /bin/bash -c " 82 | # set -ex 83 | # gem install pkg/litestream-*.gem 84 | # litestream version 85 | # " 86 | 87 | darwin-x86_64-install: 88 | needs: ["package"] 89 | runs-on: macos-13 90 | steps: 91 | - uses: ruby/setup-ruby@v1 92 | with: 93 | ruby-version: "3.2" 94 | - uses: actions/download-artifact@v4 95 | with: 96 | name: gem-x86_64-darwin 97 | path: pkg 98 | - run: "gem install pkg/litestream-*.gem" 99 | - run: "litestream version" 100 | 101 | darwin-arm64-install: 102 | needs: ["package"] 103 | runs-on: macos-14 104 | steps: 105 | - uses: ruby/setup-ruby@v1 106 | with: 107 | ruby-version: "3.2" 108 | - uses: actions/download-artifact@v4 109 | with: 110 | name: gem-arm64-darwin 111 | path: pkg 112 | - run: "gem install pkg/litestream-*.gem" 113 | - run: "litestream version" 114 | -------------------------------------------------------------------------------- /rakelib/package.rake: -------------------------------------------------------------------------------- 1 | # 2 | # Rake tasks to manage native gem packages with binary executables from benbjohnson/litestream 3 | # 4 | # TL;DR: run "rake package" 5 | # 6 | # The native platform gems (defined by Litestream::Upstream::NATIVE_PLATFORMS) will each contain 7 | # two files in addition to what the vanilla ruby gem contains: 8 | # 9 | # exe/ 10 | # ├── litestream # generic ruby script to find and run the binary 11 | # └── / 12 | # └── litestream # the litestream binary executable 13 | # 14 | # The ruby script `exe/litestream` is installed into the user's path, and it simply locates the 15 | # binary and executes it. Note that this script is required because rubygems requires that 16 | # executables declared in a gemspec must be Ruby scripts. 17 | # 18 | # As a concrete example, an x86_64-linux system will see these files on disk after installing 19 | # litestream-0.x.x-x86_64-linux.gem: 20 | # 21 | # exe/ 22 | # ├── litestream 23 | # └── x86_64-linux/ 24 | # └── litestream 25 | # 26 | # So the full set of gem files created will be: 27 | # 28 | # - pkg/litestream-1.0.0.gem 29 | # - pkg/litestream-1.0.0-aarch64-linux.gem 30 | # - pkg/litestream-1.0.0-arm64-linux.gem 31 | # - pkg/litestream-1.0.0-arm64-darwin.gem 32 | # - pkg/litestream-1.0.0-x86_64-darwin.gem 33 | # - pkg/litestream-1.0.0-x86_64-linux.gem 34 | # 35 | # Note that in addition to the native gems, a vanilla "ruby" gem will also be created without 36 | # either the `exe/litestream` script or a binary executable present. 37 | # 38 | # 39 | # New rake tasks created: 40 | # 41 | # - rake gem:ruby # Build the ruby gem 42 | # - rake gem:aarch64-linux # Build the aarch64-linux gem 43 | # - rake gem:arm64-linux # Build the arm64-linux gem 44 | # - rake gem:arm64-darwin # Build the arm64-darwin gem 45 | # - rake gem:x86_64-darwin # Build the x86_64-darwin gem 46 | # - rake gem:x86_64-linux # Build the x86_64-linux gem 47 | # - rake download # Download all litestream binaries 48 | # 49 | # Modified rake tasks: 50 | # 51 | # - rake gem # Build all the gem files 52 | # - rake package # Build all the gem files (same as `gem`) 53 | # - rake repackage # Force a rebuild of all the gem files 54 | # 55 | # Note also that the binary executables will be lazily downloaded when needed, but you can 56 | # explicitly download them with the `rake download` command. 57 | # 58 | require "rubygems/package" 59 | require "rubygems/package_task" 60 | require "open-uri" 61 | require "zlib" 62 | require "zip" 63 | require_relative "../lib/litestream/upstream" 64 | 65 | def litestream_download_url(filename) 66 | "https://github.com/benbjohnson/litestream/releases/download/#{Litestream::Upstream::VERSION}/#{filename}" 67 | end 68 | 69 | LITESTREAM_RAILS_GEMSPEC = Bundler.load_gemspec("litestream.gemspec") 70 | 71 | gem_path = Gem::PackageTask.new(LITESTREAM_RAILS_GEMSPEC).define 72 | desc "Build the ruby gem" 73 | task "gem:ruby" => [gem_path] 74 | 75 | exepaths = [] 76 | Litestream::Upstream::NATIVE_PLATFORMS.each do |platform, filename| 77 | LITESTREAM_RAILS_GEMSPEC.dup.tap do |gemspec| 78 | exedir = File.join(gemspec.bindir, platform) # "exe/x86_64-linux" 79 | exepath = File.join(exedir, "litestream") # "exe/x86_64-linux/litestream" 80 | exepaths << exepath 81 | 82 | # modify a copy of the gemspec to include the native executable 83 | gemspec.platform = platform 84 | gemspec.files += [exepath, "LICENSE-DEPENDENCIES"] 85 | 86 | # create a package task 87 | gem_path = Gem::PackageTask.new(gemspec).define 88 | desc "Build the #{platform} gem" 89 | task "gem:#{platform}" => [gem_path] 90 | 91 | directory exedir 92 | file exepath => [exedir] do 93 | release_url = litestream_download_url(filename) 94 | warn "Downloading #{exepath} from #{release_url} ..." 95 | 96 | # lazy, but fine for now. 97 | URI.open(release_url) do |remote| # standard:disable Security/Open 98 | if release_url.end_with?(".zip") 99 | Zip::File.open_buffer(remote) do |zip_file| 100 | zip_file.extract("litestream", exepath) 101 | end 102 | elsif release_url.end_with?(".gz") 103 | Zlib::GzipReader.wrap(remote) do |gz| 104 | Gem::Package::TarReader.new(gz) do |reader| 105 | reader.seek("litestream") do |file| 106 | File.binwrite(exepath, file.read) 107 | end 108 | end 109 | end 110 | end 111 | end 112 | FileUtils.chmod(0o755, exepath, verbose: true) 113 | end 114 | end 115 | end 116 | 117 | desc "Download all litestream binaries" 118 | task "download" => exepaths 119 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at stephen.margheim@gmail.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /app/views/litestream/processes/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Litestream 5 | 6 | <% if @process[:status] == "sleeping" %> 7 | 8 | <%= @process[:status] %> 9 | 10 | <% elsif @process[:status] %> 11 | 12 | <%= @process[:status] %> 13 | 14 | <% else %> 15 | 16 | not running 17 | 18 | <% end %> 19 |

20 | 21 | <% if @process[:status] %> 22 | 23 | #<%= @process[:pid] %> 24 | 25 | <% end %> 26 |
27 | 28 | <% if @process[:status] %> 29 |
30 |
Started at
31 |
32 | 33 | 34 | 35 |
36 |
37 | <% end %> 38 |
39 |
40 |
41 | 42 |
43 |
44 |

Databases

45 |

Total: <%= @databases.size %>

46 |
47 | 48 |
    49 | <% @databases.each do |database| %> 50 |
  • 51 |
    52 |

    53 | <%= database['path'] %> 54 |

    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 |
    57 | 58 |
    59 |
    60 | <% database['generations'].each do |generation| %> 61 |
    62 | 63 | <%= generation['generation'] %> 64 | (<%= generation['lag'] %> lag) 65 | 66 | 67 |
    68 |
    Start
    69 |
    70 | 71 | 72 | 73 |
    74 | 75 |
    End
    76 |
    77 | 78 | 79 | 80 |
    81 | 82 |
    83 |
    Snapshots
    84 |
    85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | <% generation['snapshots'].each do |snapshot| %> 96 | 97 | 102 | 105 | 108 | 109 | <% end %> 110 | 111 |
    Created atIndexSize
    98 | 99 | 100 | 101 | 103 | <%= snapshot['index'] %> 104 | 106 | <%= number_to_human_size snapshot['size'] %> 107 |
    112 |
    113 |
    114 |
    115 |
    116 | <% end %> 117 |
    118 |
  • 119 | <% end %> 120 |
121 |
122 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [0.14.0] - 2025-06-14 4 | 5 | - Change async behaviour of replicate and other commands ([@hschne](https://github.com/fractaledmind/litestream-ruby/pull/62)) 6 | 7 | ## [0.13.0] - 2025-06-03 8 | 9 | - Adds ability to configure default config path ([@rossta](https://github.com/fractaledmind/litestream-ruby/pull/54)) 10 | - Fix replication process detection ([@hschne](https://github.com/fractaledmind/litestream-ruby/pull/63)) 11 | - Remove locale check ([@hschne](https://github.com/fractaledmind/litestream-ruby/pull/64)) 12 | - Make base controller class configurable ([@zachasme](https://github.com/fractaledmind/litestream-ruby/pull/60)) 13 | - Support configuring replica region and endpoint ([@MatheusRich](https://github.com/fractaledmind/litestream-ruby/pull/58)) 14 | - configurable sleep time for Litestream.verify! ([@spinosa](https://github.com/fractaledmind/litestream-ruby/pull/59)) 15 | - docs: update README for restoration ([@oandalib](https://github.com/fractaledmind/litestream-ruby/pull/52)) 16 | - Build the aarch64-linux gem ([@fcatuhe](https://github.com/fractaledmind/litestream-ruby/pull/56)) 17 | - Add dark mode to dashboard ([@visini](https://github.com/fractaledmind/litestream-ruby/pull/47)) 18 | 19 | ## [0.12.0] - 2024-09-06 20 | 21 | - Add `wal` command ([alxvernier](https://github.com/fractaledmind/litestream-ruby/pull/41)) 22 | - Support configuration of custom `systemctl status` command ([rossta](https://github.com/fractaledmind/litestream-ruby/pull/39)) 23 | - Fix litestream showing as "not running" in Docker ([AxelTheGerman](https://github.com/fractaledmind/litestream-ruby/pull/44)) 24 | Update config example in README ([jgsheppa](https://github.com/fractaledmind/litestream-ruby/pull/45)) 25 | 26 | ## [0.11.2] - 2024-09-06 27 | 28 | - Simplify the getters to not use memoization 29 | 30 | ## [0.11.1] - 2024-09-06 31 | 32 | - Ensure the litestream initializer handles `nil`s 33 | 34 | ## [0.11.0] - 2024-06-21 35 | 36 | - Add a default username for the Litestream engine ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/commit/91c4de8b85be01f8cfd0cc2bf0027a6c0d9f3aaf)) 37 | - Add a verification job ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/36)) 38 | 39 | ## [0.10.5] - 2024-06-21 40 | 41 | - Fix Litestream.replicate_process for `systemd` ([@rossta](https://github.com/fractaledmind/litestream-ruby/pull/32)) 42 | 43 | ## [0.10.4] - 2024-06-21 44 | 45 | - Make engine available in published gem pkg ([@rossta](https://github.com/fractaledmind/litestream-ruby/pull/31)) 46 | 47 | ## [0.10.3] - 2024-06-10 48 | 49 | - Loading Rake tasks in the engine has them execute twice, so remove 50 | 51 | ## [0.10.2] - 2024-06-10 52 | 53 | - Fix whatever weird thing is up with "e.g." breaking the Rake task descriptions 54 | 55 | ## [0.10.1] - 2024-05-05 56 | 57 | - Ensure `verify!` reports the database that failed and returns true if verification passes ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/30)) 58 | 59 | ## [0.10.0] - 2024-05-05 60 | 61 | - Remove the verification command and Rake task and replace with a better `Litestream.verify!` method ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/28)) 62 | - Add a mountable engine for a web dashboard overview of the Litestream process ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/29)) 63 | - Add a Puma plugin ([@zachasme](https://github.com/fractaledmind/litestream-ruby/pull/22)) 64 | 65 | ## [0.9.0] - 2024-05-04 66 | 67 | - Improve the verification task by exiting with a proper status code and printing out a more clear message ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/27)) 68 | 69 | ## [0.8.0] - 2024-05-02 70 | 71 | - Improve the verification task by returning number of tables, indexes, and rows ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/26)) 72 | 73 | ## [0.7.2] - 2024-05-02 74 | 75 | - Ensure that the `Logfmt` gem is available to parse the Litestream command output 76 | 77 | ## [0.7.1] - 2024-05-02 78 | 79 | - Fix typo in executing Litestream commands 80 | 81 | ## [0.7.0] - 2024-05-02 82 | 83 | - Commands return parsed Litestream output or error if Litestream errors ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/25)) 84 | 85 | ## [0.6.0] - 2024-04-30 86 | 87 | - Don't provide a default output for the restore command, only for the verify command ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/24)) 88 | - Remove a typo from upstream.rb ([@skatkov](https://github.com/fractaledmind/litestream-ruby/pull/21)) 89 | 90 | ## [0.5.5] - 2024-04-30 91 | 92 | - Fix bug with forwarding arguments to the Rake tasks being symbols when passed to `exec` ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/23)) 93 | 94 | ## [0.5.4] - 2024-04-18 95 | 96 | - Remove old usage of config.database_path ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/18)) 97 | - Ensure that executing a command synchronously returns output ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/20)) 98 | 99 | ## [0.5.3] - 2024-04-17 100 | 101 | - Fix bug with Rake tasks not handling new kwarg method signatures of commands 102 | 103 | ## [0.5.2] - 2024-04-17 104 | 105 | - Add a `verify` command and Rake task ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/16)) 106 | - Allow any command to be run either synchronously or asynchronously ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/17)) 107 | 108 | ## [0.5.1] - 2024-04-17 109 | 110 | - Add `databases`, `generations`, and `snapshots` commands ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/15)) 111 | 112 | ## [0.5.0] - 2024-04-17 113 | 114 | - Add a `restore` command ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/14)) 115 | - Ensure that the #replicate method only sets unset ENV vars and doesn't overwrite them ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/13)) 116 | 117 | ## [0.4.0] - 2024-04-12 118 | 119 | - Generate config file with support for multiple databases ([@fractaledmind](https://github.com/fractaledmind/litestream-ruby/pull/7)) 120 | 121 | ## [0.3.3] - 2024-01-06 122 | 123 | - Fork the Litestream process to minimize memory overhead ([@supermomonga](https://github.com/fractaledmind/litestream-ruby/pull/6)) 124 | 125 | ## [0.1.0] - 2023-12-11 126 | 127 | - Initial release 128 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | litestream (0.14.0) 5 | actionpack (>= 7.0) 6 | actionview (>= 7.0) 7 | activejob (>= 7.0) 8 | activesupport (>= 7.0) 9 | railties (>= 7.0) 10 | sqlite3 11 | 12 | GEM 13 | remote: https://rubygems.org/ 14 | specs: 15 | actioncable (8.0.2) 16 | actionpack (= 8.0.2) 17 | activesupport (= 8.0.2) 18 | nio4r (~> 2.0) 19 | websocket-driver (>= 0.6.1) 20 | zeitwerk (~> 2.6) 21 | actionmailbox (8.0.2) 22 | actionpack (= 8.0.2) 23 | activejob (= 8.0.2) 24 | activerecord (= 8.0.2) 25 | activestorage (= 8.0.2) 26 | activesupport (= 8.0.2) 27 | mail (>= 2.8.0) 28 | actionmailer (8.0.2) 29 | actionpack (= 8.0.2) 30 | actionview (= 8.0.2) 31 | activejob (= 8.0.2) 32 | activesupport (= 8.0.2) 33 | mail (>= 2.8.0) 34 | rails-dom-testing (~> 2.2) 35 | actionpack (8.0.2) 36 | actionview (= 8.0.2) 37 | activesupport (= 8.0.2) 38 | nokogiri (>= 1.8.5) 39 | rack (>= 2.2.4) 40 | rack-session (>= 1.0.1) 41 | rack-test (>= 0.6.3) 42 | rails-dom-testing (~> 2.2) 43 | rails-html-sanitizer (~> 1.6) 44 | useragent (~> 0.16) 45 | actiontext (8.0.2) 46 | actionpack (= 8.0.2) 47 | activerecord (= 8.0.2) 48 | activestorage (= 8.0.2) 49 | activesupport (= 8.0.2) 50 | globalid (>= 0.6.0) 51 | nokogiri (>= 1.8.5) 52 | actionview (8.0.2) 53 | activesupport (= 8.0.2) 54 | builder (~> 3.1) 55 | erubi (~> 1.11) 56 | rails-dom-testing (~> 2.2) 57 | rails-html-sanitizer (~> 1.6) 58 | activejob (8.0.2) 59 | activesupport (= 8.0.2) 60 | globalid (>= 0.3.6) 61 | activemodel (8.0.2) 62 | activesupport (= 8.0.2) 63 | activerecord (8.0.2) 64 | activemodel (= 8.0.2) 65 | activesupport (= 8.0.2) 66 | timeout (>= 0.4.0) 67 | activestorage (8.0.2) 68 | actionpack (= 8.0.2) 69 | activejob (= 8.0.2) 70 | activerecord (= 8.0.2) 71 | activesupport (= 8.0.2) 72 | marcel (~> 1.0) 73 | activesupport (8.0.2) 74 | base64 75 | benchmark (>= 0.3) 76 | bigdecimal 77 | concurrent-ruby (~> 1.0, >= 1.3.1) 78 | connection_pool (>= 2.2.5) 79 | drb 80 | i18n (>= 1.6, < 2) 81 | logger (>= 1.4.2) 82 | minitest (>= 5.1) 83 | securerandom (>= 0.3) 84 | tzinfo (~> 2.0, >= 2.0.5) 85 | uri (>= 0.13.1) 86 | ast (2.4.2) 87 | base64 (0.2.0) 88 | benchmark (0.4.0) 89 | bigdecimal (3.1.7) 90 | builder (3.2.4) 91 | concurrent-ruby (1.3.5) 92 | connection_pool (2.4.1) 93 | crass (1.0.6) 94 | date (3.4.1) 95 | drb (2.2.1) 96 | erubi (1.12.0) 97 | globalid (1.2.1) 98 | activesupport (>= 6.1) 99 | i18n (1.14.4) 100 | concurrent-ruby (~> 1.0) 101 | io-console (0.7.2) 102 | irb (1.15.2) 103 | pp (>= 0.6.0) 104 | rdoc (>= 4.0.0) 105 | reline (>= 0.4.2) 106 | json (2.7.2) 107 | language_server-protocol (3.17.0.3) 108 | lint_roller (1.1.0) 109 | logger (1.7.0) 110 | loofah (2.22.0) 111 | crass (~> 1.0.2) 112 | nokogiri (>= 1.12.0) 113 | mail (2.8.1) 114 | mini_mime (>= 0.1.1) 115 | net-imap 116 | net-pop 117 | net-smtp 118 | marcel (1.0.4) 119 | mini_mime (1.1.5) 120 | minitest (5.22.3) 121 | net-imap (0.5.8) 122 | date 123 | net-protocol 124 | net-pop (0.1.2) 125 | net-protocol 126 | net-protocol (0.2.2) 127 | timeout 128 | net-smtp (0.5.1) 129 | net-protocol 130 | nio4r (2.7.4) 131 | nokogiri (1.18.8-arm64-darwin) 132 | racc (~> 1.4) 133 | nokogiri (1.18.8-x86_64-linux-gnu) 134 | racc (~> 1.4) 135 | parallel (1.24.0) 136 | parser (3.3.0.5) 137 | ast (~> 2.4.1) 138 | racc 139 | pp (0.6.2) 140 | prettyprint 141 | prettyprint (0.2.0) 142 | psych (5.1.2) 143 | stringio 144 | racc (1.7.3) 145 | rack (3.0.10) 146 | rack-session (2.0.0) 147 | rack (>= 3.0.0) 148 | rack-test (2.1.0) 149 | rack (>= 1.3) 150 | rackup (2.1.0) 151 | rack (>= 3) 152 | webrick (~> 1.8) 153 | rails (8.0.2) 154 | actioncable (= 8.0.2) 155 | actionmailbox (= 8.0.2) 156 | actionmailer (= 8.0.2) 157 | actionpack (= 8.0.2) 158 | actiontext (= 8.0.2) 159 | actionview (= 8.0.2) 160 | activejob (= 8.0.2) 161 | activemodel (= 8.0.2) 162 | activerecord (= 8.0.2) 163 | activestorage (= 8.0.2) 164 | activesupport (= 8.0.2) 165 | bundler (>= 1.15.0) 166 | railties (= 8.0.2) 167 | rails-dom-testing (2.2.0) 168 | activesupport (>= 5.0.0) 169 | minitest 170 | nokogiri (>= 1.6) 171 | rails-html-sanitizer (1.6.0) 172 | loofah (~> 2.21) 173 | nokogiri (~> 1.14) 174 | railties (8.0.2) 175 | actionpack (= 8.0.2) 176 | activesupport (= 8.0.2) 177 | irb (~> 1.13) 178 | rackup (>= 1.0.0) 179 | rake (>= 12.2) 180 | thor (~> 1.0, >= 1.2.2) 181 | zeitwerk (~> 2.6) 182 | rainbow (3.1.1) 183 | rake (13.2.1) 184 | rdoc (6.6.3.1) 185 | psych (>= 4.0.0) 186 | regexp_parser (2.9.0) 187 | reline (0.5.2) 188 | io-console (~> 0.5) 189 | rexml (3.2.6) 190 | rubocop (1.62.1) 191 | json (~> 2.3) 192 | language_server-protocol (>= 3.17.0) 193 | parallel (~> 1.10) 194 | parser (>= 3.3.0.2) 195 | rainbow (>= 2.2.2, < 4.0) 196 | regexp_parser (>= 1.8, < 3.0) 197 | rexml (>= 3.2.5, < 4.0) 198 | rubocop-ast (>= 1.31.1, < 2.0) 199 | ruby-progressbar (~> 1.7) 200 | unicode-display_width (>= 2.4.0, < 3.0) 201 | rubocop-ast (1.31.2) 202 | parser (>= 3.3.0.4) 203 | rubocop-performance (1.20.2) 204 | rubocop (>= 1.48.1, < 2.0) 205 | rubocop-ast (>= 1.30.0, < 2.0) 206 | ruby-progressbar (1.13.0) 207 | rubyzip (2.3.2) 208 | securerandom (0.4.1) 209 | sqlite3 (2.6.0-arm64-darwin) 210 | sqlite3 (2.6.0-x86_64-linux-gnu) 211 | standard (1.35.1) 212 | language_server-protocol (~> 3.17.0.2) 213 | lint_roller (~> 1.0) 214 | rubocop (~> 1.62.0) 215 | standard-custom (~> 1.0.0) 216 | standard-performance (~> 1.3) 217 | standard-custom (1.0.2) 218 | lint_roller (~> 1.0) 219 | rubocop (~> 1.50) 220 | standard-performance (1.3.1) 221 | lint_roller (~> 1.1) 222 | rubocop-performance (~> 1.20.2) 223 | stringio (3.1.0) 224 | thor (1.3.1) 225 | timeout (0.4.3) 226 | tzinfo (2.0.6) 227 | concurrent-ruby (~> 1.0) 228 | unicode-display_width (2.5.0) 229 | uri (1.0.3) 230 | useragent (0.16.11) 231 | webrick (1.8.1) 232 | websocket-driver (0.8.0) 233 | base64 234 | websocket-extensions (>= 0.1.0) 235 | websocket-extensions (0.1.5) 236 | zeitwerk (2.6.13) 237 | 238 | PLATFORMS 239 | arm64-darwin 240 | x86_64-linux 241 | 242 | DEPENDENCIES 243 | litestream! 244 | minitest (~> 5.0) 245 | rails 246 | rake (~> 13.0) 247 | rubyzip 248 | standard (~> 1.3) 249 | 250 | BUNDLED WITH 251 | 2.4.19 252 | -------------------------------------------------------------------------------- /lib/litestream.rb: -------------------------------------------------------------------------------- 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_region, :replica_endpoint, :replica_key_id, :replica_access_key, :systemctl_command, :config_path 37 | mattr_accessor :base_controller_class, default: "::ApplicationController" 38 | 39 | class << self 40 | def verify!(database_path, replication_sleep: 10) 41 | database = SQLite3::Database.new(database_path) 42 | database.execute("CREATE TABLE IF NOT EXISTS _litestream_verification (id INTEGER PRIMARY KEY, uuid BLOB)") 43 | sentinel = SecureRandom.uuid 44 | database.execute("INSERT INTO _litestream_verification (uuid) VALUES (?)", [sentinel]) 45 | # give the Litestream replication process time to replicate the sentinel value 46 | sleep replication_sleep 47 | 48 | backup_path = "tmp/#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_#{sentinel}.sqlite3" 49 | Litestream::Commands.restore(database_path, **{"-o" => backup_path}) 50 | 51 | backup = SQLite3::Database.new(backup_path) 52 | result = backup.execute("SELECT 1 FROM _litestream_verification WHERE uuid = ? LIMIT 1", sentinel) # => [[1]] || [] 53 | 54 | raise VerificationFailure, "Verification failed for `#{database_path}`" if result.empty? 55 | 56 | true 57 | ensure 58 | database.execute("DELETE FROM _litestream_verification WHERE uuid = ?", sentinel) 59 | database.close 60 | Dir.glob(backup_path + "*").each { |file| File.delete(file) } 61 | end 62 | 63 | # use method instead of attr_accessor to ensure 64 | # this works if variable set after Litestream is loaded 65 | def username 66 | ENV["LITESTREAM_USERNAME"] || @@username || "litestream" 67 | end 68 | 69 | def password 70 | ENV["LITESTREAM_PASSWORD"] || @@password 71 | end 72 | 73 | def queue 74 | ENV["LITESTREAM_QUEUE"] || @@queue || "default" 75 | end 76 | 77 | def replica_bucket 78 | @@replica_bucket || configuration.replica_bucket 79 | end 80 | 81 | def replica_region 82 | @@replica_region 83 | end 84 | 85 | def replica_endpoint 86 | @@replica_endpoint 87 | end 88 | 89 | def replica_key_id 90 | @@replica_key_id || configuration.replica_key_id 91 | end 92 | 93 | def replica_access_key 94 | @@replica_access_key || configuration.replica_access_key 95 | end 96 | 97 | def systemctl_command 98 | @@systemctl_command || "systemctl status litestream" 99 | end 100 | 101 | def config_path 102 | @@config_path || Rails.root.join("config", "litestream.yml") 103 | end 104 | 105 | def replicate_process 106 | systemctl_info || process_info || {} 107 | end 108 | 109 | def databases 110 | databases = Commands.databases 111 | 112 | databases.each do |db| 113 | generations = Commands.generations(db["path"]) 114 | snapshots = Commands.snapshots(db["path"]) 115 | db["path"] = db["path"].gsub(Rails.root.to_s, "[ROOT]") 116 | 117 | db["generations"] = generations.map do |generation| 118 | id = generation["generation"] 119 | replica = generation["name"] 120 | generation["snapshots"] = snapshots.select { |snapshot| snapshot["generation"] == id && snapshot["replica"] == replica } 121 | .map { |s| s.slice("index", "size", "created") } 122 | generation.slice("generation", "name", "lag", "start", "end", "snapshots") 123 | end 124 | end 125 | end 126 | 127 | private 128 | 129 | def systemctl_info 130 | return if `which systemctl`.empty? 131 | 132 | systemctl_output = `#{Litestream.systemctl_command}` 133 | systemctl_exit_code = $?.exitstatus 134 | return unless systemctl_exit_code.zero? 135 | 136 | # ["● litestream.service - Litestream", 137 | # " Loaded: loaded (/lib/systemd/system/litestream.service; enabled; vendor preset: enabled)", 138 | # " Active: active (running) since Tue 2023-07-25 13:49:43 UTC; 8 months 24 days ago", 139 | # " Main PID: 1179656 (litestream)", 140 | # " Tasks: 9 (limit: 1115)", 141 | # " Memory: 22.9M", 142 | # " CPU: 10h 49.843s", 143 | # " CGroup: /system.slice/litestream.service", 144 | # " └─1179656 /usr/bin/litestream replicate", 145 | # "", 146 | # "Warning: some journal files were not opened due to insufficient permissions."] 147 | 148 | info = {} 149 | systemctl_output.chomp.split("\n").each do |line| 150 | line.strip! 151 | if line.start_with?("Main PID:") 152 | _key, value = line.split(":") 153 | pid, _name = value.strip.split(" ") 154 | info[:pid] = pid 155 | elsif line.start_with?("Active:") 156 | value, _ago = line.split(";") 157 | status, timestamp = value.split(" since ") 158 | info[:started] = DateTime.strptime(timestamp.strip, "%a %Y-%m-%d %H:%M:%S %Z") 159 | status_match = status.match(%r{\((?.*)\)}) 160 | info[:status] = status_match ? status_match[:status] : nil 161 | end 162 | end 163 | info 164 | end 165 | 166 | def process_info 167 | litestream_replicate_ps = `ps -ax | grep litestream | grep replicate` 168 | exit_code = $?.exitstatus 169 | return unless exit_code.zero? 170 | 171 | info = {} 172 | litestream_replicate_ps.chomp.split("\n").each do |line| 173 | next unless line.include?("litestream replicate") 174 | 175 | pid, * = line.split(" ") 176 | info[:pid] = pid 177 | state, _, lstart = `ps -o "state,lstart" #{pid}`.chomp.split("\n").last.partition(/\s+/) 178 | 179 | info[:status] = case state[0] 180 | when "I" then "idle" 181 | when "R" then "running" 182 | when "S" then "sleeping" 183 | when "T" then "stopped" 184 | when "U" then "uninterruptible" 185 | when "Z" then "zombie" 186 | end 187 | info[:started] = DateTime.strptime(lstart.strip, "%a %b %d %H:%M:%S %Y") 188 | end 189 | info 190 | end 191 | end 192 | end 193 | 194 | require_relative "litestream/version" 195 | require_relative "litestream/upstream" 196 | require_relative "litestream/commands" 197 | require_relative "litestream/engine" if defined?(::Rails::Engine) 198 | -------------------------------------------------------------------------------- /lib/litestream/commands.rb: -------------------------------------------------------------------------------- 1 | require_relative "upstream" 2 | 3 | module Litestream 4 | module Commands 5 | DEFAULT_DIR = File.expand_path(File.join(__dir__, "..", "..", "exe")) 6 | GEM_NAME = "litestream" 7 | 8 | # raised when the host platform is not supported by upstream litestream's binary releases 9 | UnsupportedPlatformException = Class.new(StandardError) 10 | 11 | # raised when the litestream executable could not be found where we expected it to be 12 | ExecutableNotFoundException = Class.new(StandardError) 13 | 14 | # raised when LITESTREAM_INSTALL_DIR does not exist 15 | DirectoryNotFoundException = Class.new(StandardError) 16 | 17 | # raised when a litestream command requires a database argument but it isn't provided 18 | DatabaseRequiredException = Class.new(StandardError) 19 | 20 | # raised when a litestream command fails 21 | CommandFailedException = Class.new(StandardError) 22 | 23 | module Output 24 | class << self 25 | def format(data) 26 | return "" if data.nil? || data.empty? 27 | 28 | headers = data.first.keys.map(&:to_s) 29 | widths = headers.map.with_index { |h, i| 30 | [h.length, data.map { |r| r[data.first.keys[i]].to_s.length }.max].max 31 | } 32 | 33 | format_str = widths.map { |w| "%-#{w}s" }.join(" ") 34 | ([headers] + data.map(&:values)).map { |row| 35 | sprintf(format_str, *row.map(&:to_s)) 36 | }.join("\n") 37 | end 38 | end 39 | end 40 | 41 | class << self 42 | def platform 43 | [:cpu, :os].map { |m| Gem::Platform.local.send(m) }.join("-") 44 | end 45 | 46 | def executable(exe_path: DEFAULT_DIR) 47 | litestream_install_dir = ENV["LITESTREAM_INSTALL_DIR"] 48 | if litestream_install_dir 49 | if File.directory?(litestream_install_dir) 50 | warn "NOTE: using LITESTREAM_INSTALL_DIR to find litestream executable: #{litestream_install_dir}" 51 | exe_path = litestream_install_dir 52 | exe_file = File.expand_path(File.join(litestream_install_dir, "litestream")) 53 | else 54 | raise DirectoryNotFoundException, <<~MESSAGE 55 | LITESTREAM_INSTALL_DIR is set to #{litestream_install_dir}, but that directory does not exist. 56 | MESSAGE 57 | end 58 | else 59 | if Litestream::Upstream::NATIVE_PLATFORMS.keys.none? { |p| Gem::Platform.match_gem?(Gem::Platform.new(p), GEM_NAME) } 60 | raise UnsupportedPlatformException, <<~MESSAGE 61 | litestream-ruby does not support the #{platform} platform 62 | Please install litestream following instructions at https://litestream.io/install 63 | MESSAGE 64 | end 65 | 66 | exe_file = Dir.glob(File.expand_path(File.join(exe_path, "*", "litestream"))).find do |f| 67 | Gem::Platform.match_gem?(Gem::Platform.new(File.basename(File.dirname(f))), GEM_NAME) 68 | end 69 | end 70 | 71 | if exe_file.nil? || !File.exist?(exe_file) 72 | raise ExecutableNotFoundException, <<~MESSAGE 73 | Cannot find the litestream executable for #{platform} in #{exe_path} 74 | 75 | If you're using bundler, please make sure you're on the latest bundler version: 76 | 77 | gem install bundler 78 | bundle update --bundler 79 | 80 | Then make sure your lock file includes this platform by running: 81 | 82 | bundle lock --add-platform #{platform} 83 | bundle install 84 | 85 | See `bundle lock --help` output for details. 86 | 87 | If you're still seeing this message after taking those steps, try running 88 | `bundle config` and ensure `force_ruby_platform` isn't set to `true`. See 89 | https://github.com/fractaledmind/litestream-ruby#check-bundle_force_ruby_platform 90 | for more details. 91 | MESSAGE 92 | end 93 | 94 | exe_file 95 | end 96 | 97 | # Replicate can be run either as a fork or in the same process, depending on the context. 98 | # Puma will start replication as a forked process, while running replication from a rake 99 | # tasks won't. 100 | def replicate(async: false, **argv) 101 | cmd = prepare("replicate", argv) 102 | run_replicate(cmd, async: async) 103 | rescue 104 | raise CommandFailedException, "Failed to execute `#{cmd.join(" ")}`" 105 | end 106 | 107 | def restore(database, **argv) 108 | raise DatabaseRequiredException, "database argument is required for restore command, e.g. litestream:restore -- --database=path/to/database.sqlite" if database.nil? 109 | 110 | execute("restore", argv, database, tabled_output: false) 111 | end 112 | 113 | def databases(**argv) 114 | execute("databases", argv) 115 | end 116 | 117 | def generations(database, **argv) 118 | raise DatabaseRequiredException, "database argument is required for generations command, e.g. litestream:generations -- --database=path/to/database.sqlite" if database.nil? 119 | 120 | execute("generations", argv, database) 121 | end 122 | 123 | def snapshots(database, **argv) 124 | raise DatabaseRequiredException, "database argument is required for snapshots command, e.g. litestream:snapshots -- --database=path/to/database.sqlite" if database.nil? 125 | 126 | execute("snapshots", argv, database) 127 | end 128 | 129 | def wal(database, **argv) 130 | raise DatabaseRequiredException, "database argument is required for wal command, e.g. litestream:wal -- --database=path/to/database.sqlite" if database.nil? 131 | 132 | execute("wal", argv, database) 133 | end 134 | 135 | private 136 | 137 | def execute(command, argv = {}, database = nil, tabled_output: true) 138 | cmd = prepare(command, argv, database) 139 | results = run(cmd, tabled_output: tabled_output) 140 | 141 | if Array === results && results.one? && results[0]["level"] == "ERROR" 142 | raise CommandFailedException, "Failed to execute `#{cmd.join(" ")}`; Reason: #{results[0]["error"]}" 143 | else 144 | results 145 | end 146 | end 147 | 148 | def prepare(command, argv = {}, database = nil) 149 | ENV["LITESTREAM_REPLICA_BUCKET"] ||= Litestream.replica_bucket 150 | ENV["LITESTREAM_REPLICA_REGION"] ||= Litestream.replica_region 151 | ENV["LITESTREAM_REPLICA_ENDPOINT"] ||= Litestream.replica_endpoint 152 | ENV["LITESTREAM_ACCESS_KEY_ID"] ||= Litestream.replica_key_id 153 | ENV["LITESTREAM_SECRET_ACCESS_KEY"] ||= Litestream.replica_access_key 154 | 155 | args = { 156 | "--config" => Litestream.config_path.to_s 157 | }.merge(argv.stringify_keys).to_a.flatten.compact 158 | cmd = [executable, command, *args, database].compact 159 | puts cmd.inspect if ENV["DEBUG"] 160 | 161 | cmd 162 | end 163 | 164 | def run(cmd, tabled_output:) 165 | stdout = `#{cmd.join(" ")}`.chomp 166 | return stdout unless tabled_output 167 | 168 | keys, *rows = stdout.split("\n").map { _1.split(/\s+/) } 169 | rows.map { keys.zip(_1).to_h } 170 | end 171 | 172 | def run_replicate(cmd, async:) 173 | if async 174 | exec(*cmd) if fork.nil? 175 | else 176 | # When running in-process, we capture output continuously and write to stdout. 177 | IO.popen(cmd, err: [:child, :out]) do |io| 178 | io.each_line { |line| puts line } 179 | end 180 | end 181 | end 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /test/tasks/test_litestream_tasks.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "rake" 3 | 4 | class TestLitestreamTasks < ActiveSupport::TestCase 5 | def setup 6 | Rake.application.rake_require "tasks/litestream_tasks" 7 | Rake::Task.define_task(:environment) 8 | Rake::Task["litestream:env"].reenable 9 | Rake::Task["litestream:replicate"].reenable 10 | Rake::Task["litestream:restore"].reenable 11 | Rake::Task["litestream:databases"].reenable 12 | Rake::Task["litestream:generations"].reenable 13 | Rake::Task["litestream:snapshots"].reenable 14 | Rake::Task["litestream:wal"].reenable 15 | end 16 | 17 | def teardown 18 | ARGV.replace [] 19 | end 20 | 21 | class TestEnvTask < TestLitestreamTasks 22 | def test_env_task_when_nothing_configured_prints 23 | out, _err = capture_io do 24 | Rake.application.invoke_task "litestream:env" 25 | end 26 | 27 | assert_equal <<~TXT, out 28 | LITESTREAM_REPLICA_BUCKET= 29 | LITESTREAM_REPLICA_REGION= 30 | LITESTREAM_REPLICA_ENDPOINT= 31 | LITESTREAM_ACCESS_KEY_ID= 32 | LITESTREAM_SECRET_ACCESS_KEY= 33 | TXT 34 | end 35 | end 36 | 37 | class TestReplicateTask < TestLitestreamTasks 38 | def test_replicate_task_with_no_arguments 39 | fake = Minitest::Mock.new 40 | fake.expect :call, nil, [] 41 | Litestream::Commands.stub :replicate, fake do 42 | Rake.application.invoke_task "litestream:replicate" 43 | end 44 | fake.verify 45 | end 46 | 47 | def test_replicate_task_with_arguments 48 | ARGV.replace ["--", "--no-expand-env"] 49 | fake = Minitest::Mock.new 50 | fake.expect :call, nil, [], "--no-expand-env": nil 51 | Litestream::Commands.stub :replicate, fake do 52 | Rake.application.invoke_task "litestream:replicate" 53 | end 54 | fake.verify 55 | end 56 | 57 | def test_replicate_task_with_arguments_without_separator 58 | ARGV.replace ["--no-expand-env"] 59 | fake = Minitest::Mock.new 60 | fake.expect :call, nil, [] 61 | Litestream::Commands.stub :replicate, fake do 62 | Rake.application.invoke_task "litestream:replicate" 63 | end 64 | fake.verify 65 | end 66 | end 67 | 68 | class TestRestoreTask < TestLitestreamTasks 69 | def test_restore_task_with_only_database_using_single_dash 70 | ARGV.replace ["--", "-database=db/test.sqlite3"] 71 | fake = Minitest::Mock.new 72 | fake.expect :call, [], ["db/test.sqlite3"] 73 | Litestream::Commands.stub :restore, fake do 74 | Rake.application.invoke_task "litestream:restore" 75 | end 76 | fake.verify 77 | end 78 | 79 | def test_restore_task_with_only_database_using_double_dash 80 | ARGV.replace ["--", "--database=db/test.sqlite3"] 81 | fake = Minitest::Mock.new 82 | fake.expect :call, nil, ["db/test.sqlite3"] 83 | Litestream::Commands.stub :restore, fake do 84 | Rake.application.invoke_task "litestream:restore" 85 | end 86 | fake.verify 87 | end 88 | 89 | def test_restore_task_with_arguments 90 | ARGV.replace ["--", "-database=db/test.sqlite3", "--if-db-not-exists"] 91 | fake = Minitest::Mock.new 92 | fake.expect :call, nil, ["db/test.sqlite3"], "--if-db-not-exists": nil 93 | Litestream::Commands.stub :restore, fake do 94 | Rake.application.invoke_task "litestream:restore" 95 | end 96 | fake.verify 97 | end 98 | 99 | def test_restore_task_with_arguments_without_separator 100 | ARGV.replace ["-database=db/test.sqlite3"] 101 | fake = Minitest::Mock.new 102 | fake.expect :call, nil, [nil] 103 | Litestream::Commands.stub :restore, fake do 104 | Rake.application.invoke_task "litestream:restore" 105 | end 106 | fake.verify 107 | end 108 | end 109 | 110 | class TestDatabasesTask < TestLitestreamTasks 111 | def test_databases_task_with_no_arguments 112 | fake = Minitest::Mock.new 113 | fake.expect :call, nil, [] 114 | Litestream::Commands.stub :databases, fake do 115 | Rake.application.invoke_task "litestream:databases" 116 | end 117 | fake.verify 118 | end 119 | 120 | def test_databases_task_with_arguments 121 | ARGV.replace ["--", "--no-expand-env"] 122 | fake = Minitest::Mock.new 123 | fake.expect :call, nil, [], "--no-expand-env": nil 124 | Litestream::Commands.stub :databases, fake do 125 | Rake.application.invoke_task "litestream:databases" 126 | end 127 | fake.verify 128 | end 129 | 130 | def test_databases_task_with_arguments_without_separator 131 | ARGV.replace ["--no-expand-env"] 132 | fake = Minitest::Mock.new 133 | fake.expect :call, nil, [] 134 | Litestream::Commands.stub :databases, fake do 135 | Rake.application.invoke_task "litestream:databases" 136 | end 137 | fake.verify 138 | end 139 | end 140 | 141 | class TestGenerationsTask < TestLitestreamTasks 142 | def test_generations_task_with_only_database_using_single_dash 143 | ARGV.replace ["--", "-database=db/test.sqlite3"] 144 | fake = Minitest::Mock.new 145 | fake.expect :call, nil, ["db/test.sqlite3"] 146 | Litestream::Commands.stub :generations, fake do 147 | Rake.application.invoke_task "litestream:generations" 148 | end 149 | fake.verify 150 | end 151 | 152 | def test_generations_task_with_only_database_using_double_dash 153 | ARGV.replace ["--", "--database=db/test.sqlite3"] 154 | fake = Minitest::Mock.new 155 | fake.expect :call, nil, ["db/test.sqlite3"] 156 | Litestream::Commands.stub :generations, fake do 157 | Rake.application.invoke_task "litestream:generations" 158 | end 159 | fake.verify 160 | end 161 | 162 | def test_generations_task_with_arguments 163 | ARGV.replace ["--", "-database=db/test.sqlite3", "--if-db-not-exists"] 164 | fake = Minitest::Mock.new 165 | fake.expect :call, nil, ["db/test.sqlite3"], "--if-db-not-exists": nil 166 | Litestream::Commands.stub :generations, fake do 167 | Rake.application.invoke_task "litestream:generations" 168 | end 169 | fake.verify 170 | end 171 | 172 | def test_generations_task_with_arguments_without_separator 173 | ARGV.replace ["-database=db/test.sqlite3"] 174 | fake = Minitest::Mock.new 175 | fake.expect :call, nil, [nil] 176 | Litestream::Commands.stub :generations, fake do 177 | Rake.application.invoke_task "litestream:generations" 178 | end 179 | fake.verify 180 | end 181 | end 182 | 183 | class TestSnapshotsTask < TestLitestreamTasks 184 | def test_snapshots_task_with_only_database_using_single_dash 185 | ARGV.replace ["--", "-database=db/test.sqlite3"] 186 | fake = Minitest::Mock.new 187 | fake.expect :call, nil, ["db/test.sqlite3"] 188 | Litestream::Commands.stub :snapshots, fake do 189 | Rake.application.invoke_task "litestream:snapshots" 190 | end 191 | fake.verify 192 | end 193 | 194 | def test_snapshots_task_with_only_database_using_double_dash 195 | ARGV.replace ["--", "--database=db/test.sqlite3"] 196 | fake = Minitest::Mock.new 197 | fake.expect :call, nil, ["db/test.sqlite3"] 198 | Litestream::Commands.stub :snapshots, fake do 199 | Rake.application.invoke_task "litestream:snapshots" 200 | end 201 | fake.verify 202 | end 203 | 204 | def test_snapshots_task_with_arguments 205 | ARGV.replace ["--", "-database=db/test.sqlite3", "--if-db-not-exists"] 206 | fake = Minitest::Mock.new 207 | fake.expect :call, nil, ["db/test.sqlite3"], "--if-db-not-exists": nil 208 | Litestream::Commands.stub :snapshots, fake do 209 | Rake.application.invoke_task "litestream:snapshots" 210 | end 211 | fake.verify 212 | end 213 | 214 | def test_snapshots_task_with_arguments_without_separator 215 | ARGV.replace ["-database=db/test.sqlite3"] 216 | fake = Minitest::Mock.new 217 | fake.expect :call, nil, [nil] 218 | Litestream::Commands.stub :snapshots, fake do 219 | Rake.application.invoke_task "litestream:snapshots" 220 | end 221 | fake.verify 222 | end 223 | end 224 | 225 | class TestWalTask < TestLitestreamTasks 226 | def test_wal_task_with_only_database_using_single_dash 227 | ARGV.replace ["--", "-database=db/test.sqlite3"] 228 | fake = Minitest::Mock.new 229 | fake.expect :call, nil, ["db/test.sqlite3"] 230 | Litestream::Commands.stub :wal, fake do 231 | Rake.application.invoke_task "litestream:wal" 232 | end 233 | fake.verify 234 | end 235 | 236 | def test_wal_task_with_only_database_using_double_dash 237 | ARGV.replace ["--", "--database=db/test.sqlite3"] 238 | fake = Minitest::Mock.new 239 | fake.expect :call, nil, ["db/test.sqlite3"] 240 | Litestream::Commands.stub :wal, fake do 241 | Rake.application.invoke_task "litestream:wal" 242 | end 243 | fake.verify 244 | end 245 | 246 | def test_wal_task_with_arguments 247 | ARGV.replace ["--", "-database=db/test.sqlite3", "--if-db-not-exists"] 248 | fake = Minitest::Mock.new 249 | fake.expect :call, nil, ["db/test.sqlite3"], "--if-db-not-exists": nil 250 | Litestream::Commands.stub :wal, fake do 251 | Rake.application.invoke_task "litestream:wal" 252 | end 253 | fake.verify 254 | end 255 | 256 | def test_wal_task_with_arguments_without_separator 257 | ARGV.replace ["-database=db/test.sqlite3"] 258 | fake = Minitest::Mock.new 259 | fake.expect :call, nil, [nil] 260 | Litestream::Commands.stub :wal, fake do 261 | Rake.application.invoke_task "litestream:wal" 262 | end 263 | fake.verify 264 | end 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /LICENSE-DEPENDENCIES: -------------------------------------------------------------------------------- 1 | litestream-ruby may redistribute executables from the https://github.com/benbjohnson/litestream project 2 | 3 | The license for that software can be found at https://github.com/benbjohnson/litestream/blob/main/LICENSE which is reproduced here for your convenience: 4 | 5 | Apache License 6 | Version 2.0, January 2004 7 | http://www.apache.org/licenses/ 8 | 9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, 14 | and distribution as defined by Sections 1 through 9 of this document. 15 | 16 | "Licensor" shall mean the copyright owner or entity authorized by 17 | the copyright owner that is granting the License. 18 | 19 | "Legal Entity" shall mean the union of the acting entity and all 20 | other entities that control, are controlled by, or are under common 21 | control with that entity. For the purposes of this definition, 22 | "control" means (i) the power, direct or indirect, to cause the 23 | direction or management of such entity, whether by contract or 24 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 25 | outstanding shares, or (iii) beneficial ownership of such entity. 26 | 27 | "You" (or "Your") shall mean an individual or Legal Entity 28 | exercising permissions granted by this License. 29 | 30 | "Source" form shall mean the preferred form for making modifications, 31 | including but not limited to software source code, documentation 32 | source, and configuration files. 33 | 34 | "Object" form shall mean any form resulting from mechanical 35 | transformation or translation of a Source form, including but 36 | not limited to compiled object code, generated documentation, 37 | and conversions to other media types. 38 | 39 | "Work" shall mean the work of authorship, whether in Source or 40 | Object form, made available under the License, as indicated by a 41 | copyright notice that is included in or attached to the work 42 | (an example is provided in the Appendix below). 43 | 44 | "Derivative Works" shall mean any work, whether in Source or Object 45 | form, that is based on (or derived from) the Work and for which the 46 | editorial revisions, annotations, elaborations, or other modifications 47 | represent, as a whole, an original work of authorship. For the purposes 48 | of this License, Derivative Works shall not include works that remain 49 | separable from, or merely link (or bind by name) to the interfaces of, 50 | the Work and Derivative Works thereof. 51 | 52 | "Contribution" shall mean any work of authorship, including 53 | the original version of the Work and any modifications or additions 54 | to that Work or Derivative Works thereof, that is intentionally 55 | submitted to Licensor for inclusion in the Work by the copyright owner 56 | or by an individual or Legal Entity authorized to submit on behalf of 57 | the copyright owner. For the purposes of this definition, "submitted" 58 | means any form of electronic, verbal, or written communication sent 59 | to the Licensor or its representatives, including but not limited to 60 | communication on electronic mailing lists, source code control systems, 61 | and issue tracking systems that are managed by, or on behalf of, the 62 | Licensor for the purpose of discussing and improving the Work, but 63 | excluding communication that is conspicuously marked or otherwise 64 | designated in writing by the copyright owner as "Not a Contribution." 65 | 66 | "Contributor" shall mean Licensor and any individual or Legal Entity 67 | on behalf of whom a Contribution has been received by Licensor and 68 | subsequently incorporated within the Work. 69 | 70 | 2. Grant of Copyright License. Subject to the terms and conditions of 71 | this License, each Contributor hereby grants to You a perpetual, 72 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 73 | copyright license to reproduce, prepare Derivative Works of, 74 | publicly display, publicly perform, sublicense, and distribute the 75 | Work and such Derivative Works in Source or Object form. 76 | 77 | 3. Grant of Patent License. Subject to the terms and conditions of 78 | this License, each Contributor hereby grants to You a perpetual, 79 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 80 | (except as stated in this section) patent license to make, have made, 81 | use, offer to sell, sell, import, and otherwise transfer the Work, 82 | where such license applies only to those patent claims licensable 83 | by such Contributor that are necessarily infringed by their 84 | Contribution(s) alone or by combination of their Contribution(s) 85 | with the Work to which such Contribution(s) was submitted. If You 86 | institute patent litigation against any entity (including a 87 | cross-claim or counterclaim in a lawsuit) alleging that the Work 88 | or a Contribution incorporated within the Work constitutes direct 89 | or contributory patent infringement, then any patent licenses 90 | granted to You under this License for that Work shall terminate 91 | as of the date such litigation is filed. 92 | 93 | 4. Redistribution. You may reproduce and distribute copies of the 94 | Work or Derivative Works thereof in any medium, with or without 95 | modifications, and in Source or Object form, provided that You 96 | meet the following conditions: 97 | 98 | (a) You must give any other recipients of the Work or 99 | Derivative Works a copy of this License; and 100 | 101 | (b) You must cause any modified files to carry prominent notices 102 | stating that You changed the files; and 103 | 104 | (c) You must retain, in the Source form of any Derivative Works 105 | that You distribute, all copyright, patent, trademark, and 106 | attribution notices from the Source form of the Work, 107 | excluding those notices that do not pertain to any part of 108 | the Derivative Works; and 109 | 110 | (d) If the Work includes a "NOTICE" text file as part of its 111 | distribution, then any Derivative Works that You distribute must 112 | include a readable copy of the attribution notices contained 113 | within such NOTICE file, excluding those notices that do not 114 | pertain to any part of the Derivative Works, in at least one 115 | of the following places: within a NOTICE text file distributed 116 | as part of the Derivative Works; within the Source form or 117 | documentation, if provided along with the Derivative Works; or, 118 | within a display generated by the Derivative Works, if and 119 | wherever such third-party notices normally appear. The contents 120 | of the NOTICE file are for informational purposes only and 121 | do not modify the License. You may add Your own attribution 122 | notices within Derivative Works that You distribute, alongside 123 | or as an addendum to the NOTICE text from the Work, provided 124 | that such additional attribution notices cannot be construed 125 | as modifying the License. 126 | 127 | You may add Your own copyright statement to Your modifications and 128 | may provide additional or different license terms and conditions 129 | for use, reproduction, or distribution of Your modifications, or 130 | for any such Derivative Works as a whole, provided Your use, 131 | reproduction, and distribution of the Work otherwise complies with 132 | the conditions stated in this License. 133 | 134 | 5. Submission of Contributions. Unless You explicitly state otherwise, 135 | any Contribution intentionally submitted for inclusion in the Work 136 | by You to the Licensor shall be under the terms and conditions of 137 | this License, without any additional terms or conditions. 138 | Notwithstanding the above, nothing herein shall supersede or modify 139 | the terms of any separate license agreement you may have executed 140 | with Licensor regarding such Contributions. 141 | 142 | 6. Trademarks. This License does not grant permission to use the trade 143 | names, trademarks, service marks, or product names of the Licensor, 144 | except as required for reasonable and customary use in describing the 145 | origin of the Work and reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. Unless required by applicable law or 148 | agreed to in writing, Licensor provides the Work (and each 149 | Contributor provides its Contributions) on an "AS IS" BASIS, 150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 151 | implied, including, without limitation, any warranties or conditions 152 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 153 | PARTICULAR PURPOSE. You are solely responsible for determining the 154 | appropriateness of using or redistributing the Work and assume any 155 | risks associated with Your exercise of permissions under this License. 156 | 157 | 8. Limitation of Liability. In no event and under no legal theory, 158 | whether in tort (including negligence), contract, or otherwise, 159 | unless required by applicable law (such as deliberate and grossly 160 | negligent acts) or agreed to in writing, shall any Contributor be 161 | liable to You for damages, including any direct, indirect, special, 162 | incidental, or consequential damages of any character arising as a 163 | result of this License or out of the use or inability to use the 164 | Work (including but not limited to damages for loss of goodwill, 165 | work stoppage, computer failure or malfunction, or any and all 166 | other commercial damages or losses), even if such Contributor 167 | has been advised of the possibility of such damages. 168 | 169 | 9. Accepting Warranty or Additional Liability. While redistributing 170 | the Work or Derivative Works thereof, You may choose to offer, 171 | and charge a fee for, acceptance of support, warranty, indemnity, 172 | or other liability obligations and/or rights consistent with this 173 | License. However, in accepting such obligations, You may act only 174 | on Your own behalf and on Your sole responsibility, not on behalf 175 | of any other Contributor, and only if You agree to indemnify, 176 | defend, and hold each Contributor harmless for any liability 177 | incurred by, or claims asserted against, such Contributor by reason 178 | of your accepting any such warranty or additional liability. 179 | 180 | END OF TERMS AND CONDITIONS 181 | 182 | APPENDIX: How to apply the Apache License to your work. 183 | 184 | To apply the Apache License to your work, attach the following 185 | boilerplate notice, with the fields enclosed by brackets "[]" 186 | replaced with your own identifying information. (Don't include 187 | the brackets!) The text should be enclosed in the appropriate 188 | comment syntax for the file format. We also recommend that a 189 | file or class name and description of purpose be included on the 190 | same "printed page" as the copyright notice for easier 191 | identification within third-party archives. 192 | 193 | Copyright [yyyy] [name of copyright owner] 194 | 195 | Licensed under the Apache License, Version 2.0 (the "License"); 196 | you may not use this file except in compliance with the License. 197 | You may obtain a copy of the License at 198 | 199 | http://www.apache.org/licenses/LICENSE-2.0 200 | 201 | Unless required by applicable law or agreed to in writing, software 202 | distributed under the License is distributed on an "AS IS" BASIS, 203 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 204 | See the License for the specific language governing permissions and 205 | limitations under the License. 206 | -------------------------------------------------------------------------------- /app/views/layouts/litestream/_style.html: -------------------------------------------------------------------------------- 1 | 843 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # litestream-ruby 2 | 3 |

4 | 5 | GEM Version 6 | 7 | 8 | GEM Downloads 9 | 10 | 11 | Ruby Style 12 | 13 | 14 | Tests 15 | 16 | 17 | Sponsors 18 | 19 | 20 | Ruby.Social Follow 21 | 22 | 23 | Twitter Follow 24 | 25 |

26 | 27 | [Litestream](https://litestream.io/) is a standalone streaming replication tool for SQLite. This gem provides a Ruby interface to Litestream. 28 | 29 | ## Installation 30 | 31 | Install the gem and add to the application's Gemfile by executing: 32 | 33 | ```sh 34 | bundle add litestream 35 | ``` 36 | 37 | If bundler is not being used to manage dependencies, install the gem by executing: 38 | 39 | ```sh 40 | gem install litestream 41 | ``` 42 | 43 | After installing the gem, run the installer: 44 | 45 | ```sh 46 | rails generate litestream:install 47 | ``` 48 | 49 | The installer will create a configuration file at `config/litestream.yml` and an initializer file for configuring the gem at `config/initializers/litestream.rb`. 50 | 51 | This gem wraps the standalone executable version of the [Litestream](https://litestream.io/install/source/) utility. These executables are platform specific, so there are actually separate underlying gems per platform, but the correct gem will automatically be picked for your platform. Litestream itself doesn't support Windows, so this gem doesn't either. 52 | 53 | Supported platforms are: 54 | 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) 60 | 61 | ### Using a local installation of `litestream` 62 | 63 | If you are not able to use the vendored standalone executables (for example, if you're on an unsupported platform), you can use a local installation of the `litestream` executable by setting an environment variable named `LITESTREAM_INSTALL_DIR` to the directory containing the executable. 64 | 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: 66 | 67 | ```sh 68 | LITESTREAM_INSTALL_DIR=/usr/local/bin 69 | ``` 70 | 71 | This also works with relative paths. If you've installed into your app's directory at `./.bin/litestream`: 72 | 73 | ```sh 74 | LITESTREAM_INSTALL_DIR=.bin 75 | ``` 76 | 77 | ## Usage 78 | 79 | ### Configuration 80 | 81 | You configure the Litestream executable through the [`config/litestream.yml` file](https://litestream.io/reference/config/), which is a standard Litestream configuration file as if Litestream was running in a traditional installation. 82 | 83 | The gem streamlines the configuration process by providing a default configuration file for you. This configuration file will backup all SQLite databases defined in your `config/database.yml` file to one replication bucket. In order to ensure that no secrets are stored in plain-text in your repository, this configuration file leverages Litestream's support for environment variables. Inspect which environment variables are available by running the `bin/rails litestream:env` command. 84 | 85 | The default configuration file looks like this if you only have one SQLite database: 86 | 87 | ```yaml 88 | dbs: 89 | - path: storage/production.sqlite3 90 | replicas: 91 | - type: s3 92 | path: storage/production.sqlite3 93 | bucket: $LITESTREAM_REPLICA_BUCKET 94 | access-key-id: $LITESTREAM_ACCESS_KEY_ID 95 | secret-access-key: $LITESTREAM_SECRET_ACCESS_KEY 96 | ``` 97 | 98 | 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). 99 | 100 | The gem also provides a default initializer file at `config/initializers/litestream.rb` that allows you to configure various variables referenced in the configuration file in Ruby. By providing a Ruby interface to these environment variables, you can use your preferred method of storing secrets. For example, the default generated file uses Rails' encrypted credentials to store your secrets. 101 | 102 | ```ruby 103 | # config/initializers/litestream.rb 104 | Rails.application.configure do 105 | litestream_credentials = Rails.application.credentials.litestream 106 | 107 | config.litestream.replica_bucket = litestream_credentials&.replica_bucket 108 | config.litestream.replica_key_id = litestream_credentials&.replica_key_id 109 | config.litestream.replica_access_key = litestream_credentials&.replica_access_key 110 | end 111 | ``` 112 | 113 | Outside of configuring Litestream's replication, you may also configure various other aspects of `litestream-ruby` itself. 114 | 115 | ```ruby 116 | # config/initializers/litestream.rb 117 | Rails.application.configure do 118 | # ... 119 | 120 | # Base controller used for Litestream dashboard 121 | config.litestream.base_controller_class = "MyApplicationController" 122 | # Set the location of the Litestream config 123 | config.litestream.config_path = "config/litestream.yml" 124 | end 125 | ``` 126 | 127 | However, if you need manual control over the Litestream configuration, you can edit the `config/litestream.yml` file. The full range of possible configurations are covered in Litestream's [configuration reference](https://litestream.io/reference/config/). 128 | 129 | ### Replication 130 | 131 | In order to stream changes to your configured replicas, you need to start the Litestream replication process. 132 | 133 | 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: 134 | 135 | ```ruby 136 | # Run litestream only in production. 137 | plugin :litestream if ENV.fetch("RAILS_ENV", "production") == "production" 138 | ``` 139 | 140 | 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. 141 | 142 | The simplest way to spin up a Litestream process separately from your Rails application is to use a `Procfile`: 143 | 144 | ```yaml 145 | # Procfile 146 | rails: bundle exec rails server --port $PORT 147 | litestream: bin/rails litestream:replicate 148 | ``` 149 | 150 | Alternatively, you could setup a `systemd` service to manage the Litestream replication process, but setting this up is outside the scope of this README. 151 | 152 | If you need to pass arguments through the rake task to the underlying `litestream` command, that can be done with argument forwarding: 153 | 154 | ```shell 155 | bin/rails litestream:replicate -- -exec "foreman start" 156 | ``` 157 | 158 | This example utilizes the `-exec` option available on [the `replicate` command](https://litestream.io/reference/replicate/) which provides basic process management, since Litestream will exit when the child process exits. In this example, we only launch our collection of Rails application processes (like Rails and SolidQueue, for example) after the Litestream replication process is ready. 159 | 160 | The Litestream `replicate` command supports the following options, which can be passed through the rake task: 161 | 162 | ```shell 163 | -config PATH 164 | Specifies the configuration file. 165 | Defaults to /etc/litestream.yml 166 | 167 | -exec CMD 168 | Executes a subcommand. Litestream will exit when the child 169 | process exits. Useful for simple process management. 170 | 171 | -no-expand-env 172 | Disables environment variable expansion in configuration file. 173 | ``` 174 | 175 | ### Restoration 176 | 177 | 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: 178 | 179 | > [!NOTE] 180 | > 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. 181 | 182 | 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: 183 | ```shell 184 | # DANGEROUS OPERATION, consider renaming database files instead 185 | bin/rails db:drop DISABLE_DATABASE_ENVIRONMENT_CHECK=1 186 | ``` 187 | 188 | 2. Run restore command: 189 | ```shell 190 | bin/rails litestream:restore -- --database=storage/production.sqlite3 191 | # or 192 | bundle exec rake litestream:restore -- --database=storage/production.sqlite3 193 | ``` 194 | 195 | 3. Restart your Rails application or Docker container if applicable. 196 | 197 | 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. 198 | 199 | If you need to pass arguments through the rake task to the underlying `litestream` command, that can be done with additional forwarded arguments: 200 | 201 | ```shell 202 | bin/rails litestream:restore -- --database=storage/production.sqlite3 --if-db-not-exists 203 | ``` 204 | 205 | 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: 206 | 207 | ```shell 208 | -o PATH 209 | Output path of the restored database. 210 | Defaults to original DB path. 211 | 212 | -if-db-not-exists 213 | Returns exit code of 0 if the database already exists. 214 | 215 | -if-replica-exists 216 | Returns exit code of 0 if no backups found. 217 | 218 | -parallelism NUM 219 | Determines the number of WAL files downloaded in parallel. 220 | Defaults to 8 221 | 222 | -replica NAME 223 | Restore from a specific replica. 224 | Defaults to replica with latest data. 225 | 226 | -generation NAME 227 | Restore from a specific generation. 228 | Defaults to generation with latest data. 229 | 230 | -index NUM 231 | Restore up to a specific WAL index (inclusive). 232 | Defaults to use the highest available index. 233 | 234 | -timestamp TIMESTAMP 235 | Restore to a specific point-in-time. 236 | Defaults to use the latest available backup. 237 | 238 | -config PATH 239 | Specifies the configuration file. 240 | Defaults to /etc/litestream.yml 241 | 242 | -no-expand-env 243 | Disables environment variable expansion in configuration file. 244 | ``` 245 | 246 | ### Verification 247 | 248 | You can verify the integrity of your backed-up databases using the gem's provided `Litestream.verify!` method. The method takes the path to a database file that you have configured Litestream to backup; that is, it takes one of the `path` values under the `dbs` key in your `litestream.yml` configuration file. For example, to verify the production database, you would run: 249 | 250 | ```ruby 251 | Litestream.verify! "storage/production.sqlite3" 252 | Litestream.verify!(replication_sleep: 10) "storage/production.sqlite3" 253 | ``` 254 | 255 | 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: 256 | 257 | 1. exists, 258 | 2. can be opened by SQLite, and 259 | 3. has up-to-date data. 260 | 261 | 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. 262 | 263 | > [!NOTE] 264 | > If you configure Litestream's [`sync-interval`](https://litestream.io/reference/config/#replica-settings) to be longer than the default `replication_sleep` value of 10 seconds, you will need to adjust `replication_sleep` to a value larger than `sync-interval`; otherwise, `Litestream.verify!` may appear to fail where it actually simply didn't wait long enough for replication. 265 | 266 | ### Dashboard 267 | 268 | 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: 269 | 270 | ```ruby 271 | authenticate :user, -> (user) { user.admin? } do 272 | mount Litestream::Engine, at: "/litestream" 273 | end 274 | ``` 275 | 276 | > [!NOTE] 277 | > Be sure to [secure the dashboard](#authentication) in production. 278 | 279 | #### Authentication 280 | 281 | 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. 282 | 283 | There are two ways to setup a username and password. First, you can use the `LITESTREAM_USERNAME` and `LITESTREAM_PASSWORD` environment variables: 284 | 285 | ```ruby 286 | ENV["LITESTREAM_USERNAME"] = "frodo" 287 | ENV["LITESTREAM_PASSWORD"] = "ikeptmysecrets" 288 | ``` 289 | 290 | Second, you can configure the access credentials via the Rails configuration object, under the `litestream` key, in an initializer: 291 | 292 | ```ruby 293 | # Set authentication credentials for Litestream 294 | config.litestream.username = Rails.application.credentials.dig(:litestream, :username) 295 | config.litestream.password = Rails.application.credentials.dig(:litestream, :password) 296 | ``` 297 | 298 | Either way, if you have set a username and password, Litestream will use basic HTTP authentication. 299 | 300 | > [!IMPORTANT] 301 | > If you have not set a username and password, Litestream will not require any authentication to view the dashboard. 302 | 303 | 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: 304 | 305 | ```ruby 306 | authenticate :user, -> (user) { user.admin? } do 307 | mount Litestream::Engine, at: "/litestream" 308 | end 309 | ``` 310 | 311 | ### Examples 312 | 313 | There is only one screen in the dashboard. 314 | 315 | - the show view of the Litestream replication process: 316 | 317 | ![screenshot of the single page in the web dashboard, showing details of the Litestream replication process](images/show-screenshot.png) 318 | 319 | ### Usage with API-only Applications 320 | 321 | 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: 322 | 323 | ```ruby 324 | # /config/application.rb 325 | config.middleware.use ActionDispatch::Cookies 326 | config.middleware.use ActionDispatch::Session::CookieStore 327 | config.middleware.use ActionDispatch::Flash 328 | ``` 329 | 330 | ### Overwriting the views 331 | 332 | You can find the views in [`app/views`](https://github.com/fractaledmind/litestream-ruby/tree/main/app/views). 333 | 334 | ```bash 335 | app/views/ 336 | ├── layouts 337 | │   └── litestream 338 | │   ├── _style.html 339 | │   └── application.html.erb 340 | └── litestream 341 | └── processes 342 |    └── show.html.erb 343 | ``` 344 | 345 | 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: 346 | 347 | ```erb 348 | 349 | 350 | 351 | Litestream 352 | <%= csrf_meta_tags %> 353 | <%= csp_meta_tag %> 354 | 355 | <%= render "layouts/litestream/style" %> 356 | 357 | 358 |
359 | <%= content_for?(:content) ? yield(:content) : yield %> 360 |
361 | 362 |
363 | <% if notice.present? %> 364 |

365 | <%= notice %> 366 |

367 | <% end %> 368 | 369 | <% if alert.present? %> 370 |

371 | <%= alert %> 372 |

373 | <% end %> 374 |
375 | 376 | 377 | ``` 378 | 379 | ### Introspection 380 | 381 | 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: 382 | 383 | ```shell 384 | bin/rails litestream:databases 385 | ``` 386 | 387 | This will return a list of databases and their configured replicas: 388 | 389 | ``` 390 | path replicas 391 | /Users/you/Code/your-app/storage/production.sqlite3 s3 392 | ``` 393 | 394 | You can also list the generations of a specific database: 395 | 396 | ```shell 397 | bin/rails litestream:generations -- --database=storage/production.sqlite3 398 | ``` 399 | 400 | This will list all generations for the specified database, including stats about their lag behind the primary database and the time range they cover: 401 | 402 | ``` 403 | name generation lag start end 404 | s3 a295b16a796689f3 -156ms 2024-04-17T00:01:19Z 2024-04-17T00:01:19Z 405 | ``` 406 | 407 | You can list the snapshots available for a database: 408 | 409 | ```shell 410 | bin/rails litestream:snapshots -- --database=storage/production.sqlite3 411 | ``` 412 | 413 | This command lists snapshots available for that specified database: 414 | 415 | ``` 416 | replica generation index size created 417 | s3 a295b16a796689f3 1 4645465 2024-04-17T00:01:19Z 418 | ``` 419 | 420 | Finally, you can list the wal files available for a database: 421 | 422 | ```shell 423 | bin/rails litestream:wal -- --database=storage/production.sqlite3 424 | ``` 425 | 426 | This command lists wal files available for that specified database: 427 | 428 | ``` 429 | replica generation index offset size created 430 | s3 a295b16a796689f3 1 0 2036 2024-04-17T00:01:19Z 431 | ``` 432 | 433 | ### Running commands from Ruby 434 | 435 | 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. 436 | 437 | The `Litestream::Commands.databases` method returns an array of hashes with the "path" and "replicas" keys for each database: 438 | 439 | ```ruby 440 | Litestream::Commands.databases 441 | # => [{"path"=>"/Users/you/Code/your-app/storage/production.sqlite3", "replicas"=>"s3"}] 442 | ``` 443 | 444 | The `Litestream::Commands.generations` method returns an array of hashes with the "name", "generation", "lag", "start", and "end" keys for each generation: 445 | 446 | ```ruby 447 | Litestream::Commands.generations('storage/production.sqlite3') 448 | # => [{"name"=>"s3", "generation"=>"5f4341bc3d22d615", "lag"=>"3s", "start"=>"2024-04-17T19:48:09Z", "end"=>"2024-04-17T19:48:09Z"}] 449 | ``` 450 | 451 | The `Litestream::Commands.snapshots` method returns an array of hashes with the "replica", "generation", "index", "size", and "created" keys for each snapshot: 452 | 453 | ```ruby 454 | Litestream::Commands.snapshots('storage/production.sqlite3') 455 | # => [{"replica"=>"s3", "generation"=>"5f4341bc3d22d615", "index"=>"0", "size"=>"4645465", "created"=>"2024-04-17T19:48:09Z"}] 456 | ``` 457 | 458 | The `Litestream::Commands.wal` method returns an array of hashes with the "replica", "generation", "index", "offset","size", and "created" keys for each wal: 459 | 460 | ```ruby 461 | Litestream::Commands.wal('storage/production.sqlite3') 462 | # => [{"replica"=>"s3", "generation"=>"5f4341bc3d22d615", "index"=>"0", "offset"=>"0", "size"=>"2036", "created"=>"2024-04-17T19:48:09Z"}] 463 | ``` 464 | 465 | You can also restore a database programmatically using the `Litestream::Commands.restore` method, which returns the path to the restored database: 466 | 467 | ```ruby 468 | Litestream::Commands.restore('storage/production.sqlite3') 469 | # => "storage/production-20240418090048.sqlite3" 470 | ``` 471 | 472 | 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. 473 | 474 | ### Running commands from CLI 475 | 476 | 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`. 477 | 478 | The full set of commands available to the `litestream` executable are covered in Litestream's [command reference](https://litestream.io/reference/), but can be summarized as: 479 | 480 | ```shell 481 | litestream databases [arguments] 482 | litestream generations [arguments] DB_PATH|REPLICA_URL 483 | litestream replicate [arguments] 484 | litestream restore [arguments] DB_PATH|REPLICA_URL 485 | litestream snapshots [arguments] DB_PATH|REPLICA_URL 486 | litestream version 487 | litestream wal [arguments] DB_PATH|REPLICA_URL 488 | ``` 489 | 490 | ### Using in development 491 | 492 | By default, if you install the gem and configure via `puma.rb` or `Procfile`, Litestream will not start in development. 493 | 494 | If you setup via `puma.rb`, then remove the conditional statement. 495 | 496 | 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`. 497 | 498 | 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: 499 | 500 | ```sh 501 | docker run -p 9000:9000 -p 9001:9001 minio/minio server /data --console-address ":9001" 502 | ``` 503 | 504 | This gets us up and running quickly but it will only persist the data for as long as the Docker container is running, which is fine for local development testing. 505 | 506 | To simplify local development, you can add this command to your `Procfile.dev` file as well. This would allow you to start a MinIO server and a Litestream replication process in your local development environment with the single `bin/dev` command. 507 | 508 | Once you have a MinIO server running, you can create a bucket for Litestream to use. You can do this by visiting the MinIO console at [http://localhost:9001](http://localhost:9001) and logging in with the default credentials of `minioadmin` and `minioadmin`. Once logged in, you can create a bucket named `mybkt` by clicking the `+` button in the bottom right corner of the screen. You can then use the following configuration in your `config/initializers/litestream.rb` file: 509 | 510 | ```ruby 511 | Litestream.configure do |config| 512 | config.replica_bucket = "s3://mybkt.localhost:9000/" 513 | config.replica_key_id = "minioadmin" 514 | config.replica_access_key = "minioadmin" 515 | end 516 | ``` 517 | 518 | With Litestream properly configured and the MinIO server and Litestream replication process running, you should see something like the following in your terminal logs when you start the `bin/dev` process: 519 | 520 | ```sh 521 | time=YYYY-MM-DDTHH:MM:SS level=INFO msg=litestream version=v0.3.xx 522 | time=YYYY-MM-DDTHH:MM:SS level=INFO msg="initialized db" path=/path/to/your/app/storage/development.sqlite3 523 | time=YYYY-MM-DDTHH:MM:SS level=INFO msg="replicating to" name=s3 type=s3 sync-interval=1s bucket=mybkt path="" region=us-east-1 endpoint=http://localhost:9000 524 | ``` 525 | 526 | ## Development 527 | 528 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 529 | 530 | To install this gem onto your local machine, run `bundle exec rake install`. To download the Litestream binaries run `bundle exec rake download`. 531 | 532 | For maintainers, to release a new version, run `bin/release $VERSION`, which will create a git tag for the version, push git commits and tags, and push all of the platform-specific `.gem` files to [rubygems.org](https://rubygems.org). 533 | 534 | ## Contributing 535 | 536 | Bug reports and pull requests are welcome on GitHub at https://github.com/fractaledmind/litestream-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/fractaledmind/litestream-ruby/blob/main/CODE_OF_CONDUCT.md). 537 | 538 | ## License 539 | 540 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 541 | 542 | ## Code of Conduct 543 | 544 | Everyone interacting in the Litestream project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/fractaledmind/litestream-ruby/blob/main/CODE_OF_CONDUCT.md). 545 | -------------------------------------------------------------------------------- /test/litestream/test_commands.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TestCommands < ActiveSupport::TestCase 4 | def run 5 | result = nil 6 | Litestream::Commands.stub :fork, nil do 7 | Litestream::Commands.stub :executable, "exe/test/litestream" do 8 | capture_io { result = super } 9 | end 10 | end 11 | result 12 | end 13 | 14 | def teardown 15 | Litestream.replica_bucket = ENV["LITESTREAM_REPLICA_BUCKET"] = nil 16 | Litestream.replica_key_id = ENV["LITESTREAM_ACCESS_KEY_ID"] = nil 17 | Litestream.replica_access_key = ENV["LITESTREAM_SECRET_ACCESS_KEY"] = nil 18 | Litestream.config_path = nil 19 | end 20 | 21 | class TestReplicateCommand < TestCommands 22 | def test_replicate_with_no_options 23 | stub = proc do |cmd| 24 | executable, command, *argv = cmd 25 | assert_match Regexp.new("exe/test/litestream"), executable 26 | assert_equal "replicate", command 27 | assert_equal 2, argv.size 28 | assert_equal "--config", argv[0] 29 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 30 | end 31 | Litestream::Commands.stub :run_replicate, stub do 32 | Litestream::Commands.replicate 33 | end 34 | end 35 | 36 | def test_replicate_with_boolean_option 37 | stub = proc do |cmd| 38 | executable, command, *argv = cmd 39 | assert_match Regexp.new("exe/test/litestream"), executable 40 | assert_equal "replicate", command 41 | assert_equal 3, argv.size 42 | assert_equal "--config", argv[0] 43 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 44 | assert_equal "--no-expand-env", argv[2] 45 | end 46 | Litestream::Commands.stub :run_replicate, stub do 47 | Litestream::Commands.replicate("--no-expand-env" => nil) 48 | end 49 | end 50 | 51 | def test_replicate_with_string_option 52 | stub = proc do |cmd| 53 | executable, command, *argv = cmd 54 | assert_match Regexp.new("exe/test/litestream"), executable 55 | assert_equal "replicate", command 56 | assert_equal 4, argv.size 57 | assert_equal "--config", argv[0] 58 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 59 | assert_equal "--exec", argv[2] 60 | assert_equal "command", argv[3] 61 | end 62 | Litestream::Commands.stub :run_replicate, stub do 63 | Litestream::Commands.replicate("--exec" => "command") 64 | end 65 | end 66 | 67 | def test_replicate_with_symbol_option 68 | stub = proc do |cmd| 69 | executable, command, *argv = cmd 70 | assert_match Regexp.new("exe/test/litestream"), executable 71 | assert_equal "replicate", command 72 | assert_equal 4, argv.size 73 | assert_equal "--config", argv[0] 74 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 75 | assert_equal "--exec", argv[2] 76 | assert_equal "command", argv[3] 77 | end 78 | Litestream::Commands.stub :run_replicate, stub do 79 | Litestream::Commands.replicate("--exec": "command") 80 | end 81 | end 82 | 83 | def test_replicate_with_config_option 84 | stub = proc do |cmd| 85 | executable, command, *argv = cmd 86 | assert_match Regexp.new("exe/test/litestream"), executable 87 | assert_equal "replicate", command 88 | assert_equal 2, argv.size 89 | assert_equal "--config", argv[0] 90 | assert_equal "CONFIG", argv[1] 91 | end 92 | Litestream::Commands.stub :run_replicate, stub do 93 | Litestream::Commands.replicate("--config" => "CONFIG") 94 | end 95 | end 96 | 97 | def test_replicate_sets_replica_bucket_env_var_from_config_when_env_var_not_set 98 | Litestream.replica_bucket = "mybkt" 99 | 100 | Litestream::Commands.stub :run_replicate, nil do 101 | Litestream::Commands.replicate 102 | end 103 | 104 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 105 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 106 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 107 | end 108 | 109 | def test_replicate_sets_replica_key_id_env_var_from_config_when_env_var_not_set 110 | Litestream.replica_key_id = "mykey" 111 | 112 | Litestream::Commands.stub :run_replicate, nil do 113 | Litestream::Commands.replicate 114 | end 115 | 116 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 117 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 118 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 119 | end 120 | 121 | def test_replicate_sets_replica_access_key_env_var_from_config_when_env_var_not_set 122 | Litestream.replica_access_key = "access" 123 | 124 | Litestream::Commands.stub :run_replicate, nil do 125 | Litestream::Commands.replicate 126 | end 127 | 128 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 129 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 130 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 131 | end 132 | 133 | def test_replicate_sets_all_env_vars_from_config_when_env_vars_not_set 134 | Litestream.replica_bucket = "mybkt" 135 | Litestream.replica_key_id = "mykey" 136 | Litestream.replica_access_key = "access" 137 | 138 | Litestream::Commands.stub :run_replicate, nil do 139 | Litestream::Commands.replicate 140 | end 141 | 142 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 143 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 144 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 145 | end 146 | 147 | def test_replicate_does_not_set_env_var_from_config_when_env_vars_already_set 148 | ENV["LITESTREAM_REPLICA_BUCKET"] = "original_bkt" 149 | ENV["LITESTREAM_ACCESS_KEY_ID"] = "original_key" 150 | ENV["LITESTREAM_SECRET_ACCESS_KEY"] = "original_access" 151 | 152 | Litestream.replica_bucket = "mybkt" 153 | Litestream.replica_key_id = "mykey" 154 | Litestream.replica_access_key = "access" 155 | 156 | Litestream::Commands.stub :run_replicate, nil do 157 | Litestream::Commands.replicate 158 | end 159 | 160 | assert_equal "original_bkt", ENV["LITESTREAM_REPLICA_BUCKET"] 161 | assert_equal "original_key", ENV["LITESTREAM_ACCESS_KEY_ID"] 162 | assert_equal "original_access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 163 | end 164 | end 165 | 166 | class TestRestoreCommand < TestCommands 167 | def test_restore_with_no_options 168 | stub = proc do |cmd| 169 | executable, command, *argv = cmd 170 | assert_match Regexp.new("exe/test/litestream"), executable 171 | assert_equal "restore", command 172 | assert_equal 3, argv.size 173 | assert_equal "--config", argv[0] 174 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 175 | assert_equal "db/test.sqlite3", argv[2] 176 | end 177 | Litestream::Commands.stub :run, stub do 178 | Litestream::Commands.restore("db/test.sqlite3") 179 | end 180 | end 181 | 182 | def test_restore_with_boolean_option 183 | stub = proc do |cmd| 184 | executable, command, *argv = cmd 185 | assert_match Regexp.new("exe/test/litestream"), executable 186 | assert_equal "restore", command 187 | assert_equal 4, argv.size 188 | assert_equal "--config", argv[0] 189 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 190 | assert_equal "--if-db-not-exists", argv[2] 191 | assert_equal "db/test.sqlite3", argv[3] 192 | end 193 | Litestream::Commands.stub :run, stub do 194 | Litestream::Commands.restore("db/test.sqlite3", "--if-db-not-exists" => nil) 195 | end 196 | end 197 | 198 | def test_restore_with_string_option 199 | stub = proc do |cmd| 200 | executable, command, *argv = cmd 201 | assert_match Regexp.new("exe/test/litestream"), executable 202 | assert_equal "restore", command 203 | assert_equal 5, argv.size 204 | assert_equal "--config", argv[0] 205 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 206 | assert_equal "--parallelism", argv[2] 207 | assert_equal 10, argv[3] 208 | assert_equal "db/test.sqlite3", argv[4] 209 | end 210 | Litestream::Commands.stub :run, stub do 211 | Litestream::Commands.restore("db/test.sqlite3", "--parallelism" => 10) 212 | end 213 | end 214 | 215 | def test_restore_with_config_option 216 | stub = proc do |cmd| 217 | executable, command, *argv = cmd 218 | assert_match Regexp.new("exe/test/litestream"), executable 219 | assert_equal "restore", command 220 | assert_equal 3, argv.size 221 | assert_equal "--config", argv[0] 222 | assert_equal "CONFIG", argv[1] 223 | assert_equal "db/test.sqlite3", argv[2] 224 | end 225 | Litestream::Commands.stub :run, stub do 226 | Litestream::Commands.restore("db/test.sqlite3", "--config" => "CONFIG") 227 | end 228 | end 229 | 230 | def test_restore_sets_replica_bucket_env_var_from_config_when_env_var_not_set 231 | Litestream.replica_bucket = "mybkt" 232 | 233 | Litestream::Commands.stub :run, nil do 234 | Litestream::Commands.restore("db/test.sqlite3") 235 | end 236 | 237 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 238 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 239 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 240 | end 241 | 242 | def test_restore_sets_replica_key_id_env_var_from_config_when_env_var_not_set 243 | Litestream.replica_key_id = "mykey" 244 | 245 | Litestream::Commands.stub :run, nil do 246 | Litestream::Commands.restore("db/test.sqlite3") 247 | end 248 | 249 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 250 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 251 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 252 | end 253 | 254 | def test_restore_sets_replica_access_key_env_var_from_config_when_env_var_not_set 255 | Litestream.replica_access_key = "access" 256 | 257 | Litestream::Commands.stub :run, nil do 258 | Litestream::Commands.restore("db/test.sqlite3") 259 | end 260 | 261 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 262 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 263 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 264 | end 265 | 266 | def test_restore_sets_all_env_vars_from_config_when_env_vars_not_set 267 | Litestream.replica_bucket = "mybkt" 268 | Litestream.replica_key_id = "mykey" 269 | Litestream.replica_access_key = "access" 270 | 271 | Litestream::Commands.stub :run, nil do 272 | Litestream::Commands.restore("db/test.sqlite3") 273 | end 274 | 275 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 276 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 277 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 278 | end 279 | 280 | def test_restore_does_not_set_env_var_from_config_when_env_vars_already_set 281 | ENV["LITESTREAM_REPLICA_BUCKET"] = "original_bkt" 282 | ENV["LITESTREAM_ACCESS_KEY_ID"] = "original_key" 283 | ENV["LITESTREAM_SECRET_ACCESS_KEY"] = "original_access" 284 | 285 | Litestream.replica_bucket = "mybkt" 286 | Litestream.replica_key_id = "mykey" 287 | Litestream.replica_access_key = "access" 288 | 289 | Litestream::Commands.stub :run, nil do 290 | Litestream::Commands.restore("db/test.sqlite3") 291 | end 292 | 293 | assert_equal "original_bkt", ENV["LITESTREAM_REPLICA_BUCKET"] 294 | assert_equal "original_key", ENV["LITESTREAM_ACCESS_KEY_ID"] 295 | assert_equal "original_access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 296 | end 297 | end 298 | 299 | class TestDatabasesCommand < TestCommands 300 | def test_databases_with_no_options 301 | stub = proc do |cmd| 302 | executable, command, *argv = cmd 303 | assert_match Regexp.new("exe/test/litestream"), executable 304 | assert_equal "databases", command 305 | assert_equal 2, argv.size 306 | assert_equal "--config", argv[0] 307 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 308 | end 309 | Litestream::Commands.stub :run, stub do 310 | Litestream::Commands.databases 311 | end 312 | end 313 | 314 | def test_databases_with_boolean_option 315 | stub = proc do |cmd| 316 | executable, command, *argv = cmd 317 | assert_match Regexp.new("exe/test/litestream"), executable 318 | assert_equal "databases", command 319 | assert_equal 3, argv.size 320 | assert_equal "--config", argv[0] 321 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 322 | assert_equal "--no-expand-env", argv[2] 323 | end 324 | Litestream::Commands.stub :run, stub do 325 | Litestream::Commands.databases("--no-expand-env" => nil) 326 | end 327 | end 328 | 329 | def test_databases_with_string_option 330 | stub = proc do |cmd| 331 | executable, command, *argv = cmd 332 | assert_match Regexp.new("exe/test/litestream"), executable 333 | assert_equal "databases", command 334 | assert_equal 4, argv.size 335 | assert_equal "--config", argv[0] 336 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 337 | assert_equal "--exec", argv[2] 338 | assert_equal "command", argv[3] 339 | end 340 | Litestream::Commands.stub :run, stub do 341 | Litestream::Commands.databases("--exec" => "command") 342 | end 343 | end 344 | 345 | def test_databases_with_config_option 346 | stub = proc do |cmd| 347 | executable, command, *argv = cmd 348 | assert_match Regexp.new("exe/test/litestream"), executable 349 | assert_equal "databases", command 350 | assert_equal 2, argv.size 351 | assert_equal "--config", argv[0] 352 | assert_equal "CONFIG", argv[1] 353 | end 354 | Litestream::Commands.stub :run, stub do 355 | Litestream::Commands.databases("--config" => "CONFIG") 356 | end 357 | end 358 | 359 | def test_databases_sets_replica_bucket_env_var_from_config_when_env_var_not_set 360 | Litestream.replica_bucket = "mybkt" 361 | 362 | Litestream::Commands.stub :run, nil do 363 | Litestream::Commands.databases 364 | end 365 | 366 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 367 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 368 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 369 | end 370 | 371 | def test_databases_sets_replica_key_id_env_var_from_config_when_env_var_not_set 372 | Litestream.replica_key_id = "mykey" 373 | 374 | Litestream::Commands.stub :run, nil do 375 | Litestream::Commands.databases 376 | end 377 | 378 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 379 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 380 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 381 | end 382 | 383 | def test_databases_sets_replica_access_key_env_var_from_config_when_env_var_not_set 384 | Litestream.replica_access_key = "access" 385 | 386 | Litestream::Commands.stub :run, nil do 387 | Litestream::Commands.databases 388 | end 389 | 390 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 391 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 392 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 393 | end 394 | 395 | def test_databases_sets_all_env_vars_from_config_when_env_vars_not_set 396 | Litestream.replica_bucket = "mybkt" 397 | Litestream.replica_key_id = "mykey" 398 | Litestream.replica_access_key = "access" 399 | 400 | Litestream::Commands.stub :run, nil do 401 | Litestream::Commands.databases 402 | end 403 | 404 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 405 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 406 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 407 | end 408 | 409 | def test_databases_does_not_set_env_var_from_config_when_env_vars_already_set 410 | ENV["LITESTREAM_REPLICA_BUCKET"] = "original_bkt" 411 | ENV["LITESTREAM_ACCESS_KEY_ID"] = "original_key" 412 | ENV["LITESTREAM_SECRET_ACCESS_KEY"] = "original_access" 413 | 414 | Litestream.replica_bucket = "mybkt" 415 | Litestream.replica_key_id = "mykey" 416 | Litestream.replica_access_key = "access" 417 | 418 | Litestream::Commands.stub :run, nil do 419 | Litestream::Commands.databases 420 | end 421 | 422 | assert_equal "original_bkt", ENV["LITESTREAM_REPLICA_BUCKET"] 423 | assert_equal "original_key", ENV["LITESTREAM_ACCESS_KEY_ID"] 424 | assert_equal "original_access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 425 | end 426 | 427 | def test_databases_read_from_custom_configured_litestream_config_path 428 | Litestream.config_path = "dummy/config/litestream/production.yml" 429 | 430 | stub = proc do |cmd, _async| 431 | _executable, _command, *argv = cmd 432 | 433 | assert_equal 2, argv.size 434 | assert_equal "--config", argv[0] 435 | assert_match Regexp.new("dummy/config/litestream/production.yml"), argv[1] 436 | end 437 | 438 | Litestream::Commands.stub :run, stub do 439 | Litestream::Commands.databases 440 | end 441 | end 442 | end 443 | 444 | class TestGenerationsCommand < TestCommands 445 | def test_generations_with_no_options 446 | stub = proc do |cmd| 447 | executable, command, *argv = cmd 448 | assert_match Regexp.new("exe/test/litestream"), executable 449 | assert_equal "generations", command 450 | assert_equal 3, argv.size 451 | assert_equal "--config", argv[0] 452 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 453 | assert_equal "db/test.sqlite3", argv[2] 454 | end 455 | Litestream::Commands.stub :run, stub do 456 | Litestream::Commands.generations("db/test.sqlite3") 457 | end 458 | end 459 | 460 | def test_generations_with_boolean_option 461 | stub = proc do |cmd| 462 | executable, command, *argv = cmd 463 | assert_match Regexp.new("exe/test/litestream"), executable 464 | assert_equal "generations", command 465 | assert_equal 4, argv.size 466 | assert_equal "--config", argv[0] 467 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 468 | assert_equal "--if-db-not-exists", argv[2] 469 | assert_equal "db/test.sqlite3", argv[3] 470 | end 471 | Litestream::Commands.stub :run, stub do 472 | Litestream::Commands.generations("db/test.sqlite3", "--if-db-not-exists" => nil) 473 | end 474 | end 475 | 476 | def test_generations_with_string_option 477 | stub = proc do |cmd| 478 | executable, command, *argv = cmd 479 | assert_match Regexp.new("exe/test/litestream"), executable 480 | assert_equal "generations", command 481 | assert_equal 5, argv.size 482 | assert_equal "--config", argv[0] 483 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 484 | assert_equal "--parallelism", argv[2] 485 | assert_equal 10, argv[3] 486 | assert_equal "db/test.sqlite3", argv[4] 487 | end 488 | Litestream::Commands.stub :run, stub do 489 | Litestream::Commands.generations("db/test.sqlite3", "--parallelism" => 10) 490 | end 491 | end 492 | 493 | def test_generations_with_config_option 494 | stub = proc do |cmd| 495 | executable, command, *argv = cmd 496 | assert_match Regexp.new("exe/test/litestream"), executable 497 | assert_equal "generations", command 498 | assert_equal 3, argv.size 499 | assert_equal "--config", argv[0] 500 | assert_equal "CONFIG", argv[1] 501 | assert_equal "db/test.sqlite3", argv[2] 502 | end 503 | Litestream::Commands.stub :run, stub do 504 | Litestream::Commands.generations("db/test.sqlite3", "--config" => "CONFIG") 505 | end 506 | end 507 | 508 | def test_generations_sets_replica_bucket_env_var_from_config_when_env_var_not_set 509 | Litestream.replica_bucket = "mybkt" 510 | 511 | Litestream::Commands.stub :run, nil do 512 | Litestream::Commands.generations("db/test.sqlite3") 513 | end 514 | 515 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 516 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 517 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 518 | end 519 | 520 | def test_generations_sets_replica_key_id_env_var_from_config_when_env_var_not_set 521 | Litestream.replica_key_id = "mykey" 522 | 523 | Litestream::Commands.stub :run, nil do 524 | Litestream::Commands.generations("db/test.sqlite3") 525 | end 526 | 527 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 528 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 529 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 530 | end 531 | 532 | def test_generations_sets_replica_access_key_env_var_from_config_when_env_var_not_set 533 | Litestream.replica_access_key = "access" 534 | 535 | Litestream::Commands.stub :run, nil do 536 | Litestream::Commands.generations("db/test.sqlite3") 537 | end 538 | 539 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 540 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 541 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 542 | end 543 | 544 | def test_generations_sets_all_env_vars_from_config_when_env_vars_not_set 545 | Litestream.replica_bucket = "mybkt" 546 | Litestream.replica_key_id = "mykey" 547 | Litestream.replica_access_key = "access" 548 | 549 | Litestream::Commands.stub :run, nil do 550 | Litestream::Commands.generations("db/test.sqlite3") 551 | end 552 | 553 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 554 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 555 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 556 | end 557 | 558 | def test_generations_does_not_set_env_var_from_config_when_env_vars_already_set 559 | ENV["LITESTREAM_REPLICA_BUCKET"] = "original_bkt" 560 | ENV["LITESTREAM_ACCESS_KEY_ID"] = "original_key" 561 | ENV["LITESTREAM_SECRET_ACCESS_KEY"] = "original_access" 562 | 563 | Litestream.replica_bucket = "mybkt" 564 | Litestream.replica_key_id = "mykey" 565 | Litestream.replica_access_key = "access" 566 | 567 | Litestream::Commands.stub :run, nil do 568 | Litestream::Commands.generations("db/test.sqlite3") 569 | end 570 | 571 | assert_equal "original_bkt", ENV["LITESTREAM_REPLICA_BUCKET"] 572 | assert_equal "original_key", ENV["LITESTREAM_ACCESS_KEY_ID"] 573 | assert_equal "original_access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 574 | end 575 | end 576 | 577 | class TestSnapshotsCommand < TestCommands 578 | def test_snapshots_with_no_options 579 | stub = proc do |cmd| 580 | executable, command, *argv = cmd 581 | assert_match Regexp.new("exe/test/litestream"), executable 582 | assert_equal "snapshots", command 583 | assert_equal 3, argv.size 584 | assert_equal "--config", argv[0] 585 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 586 | assert_equal "db/test.sqlite3", argv[2] 587 | end 588 | Litestream::Commands.stub :run, stub do 589 | Litestream::Commands.snapshots("db/test.sqlite3") 590 | end 591 | end 592 | 593 | def test_snapshots_with_boolean_option 594 | stub = proc do |cmd| 595 | executable, command, *argv = cmd 596 | assert_match Regexp.new("exe/test/litestream"), executable 597 | assert_equal "snapshots", command 598 | assert_equal 4, argv.size 599 | assert_equal "--config", argv[0] 600 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 601 | assert_equal "--if-db-not-exists", argv[2] 602 | assert_equal "db/test.sqlite3", argv[3] 603 | end 604 | Litestream::Commands.stub :run, stub do 605 | Litestream::Commands.snapshots("db/test.sqlite3", "--if-db-not-exists" => nil) 606 | end 607 | end 608 | 609 | def test_snapshots_with_string_option 610 | stub = proc do |cmd| 611 | executable, command, *argv = cmd 612 | assert_match Regexp.new("exe/test/litestream"), executable 613 | assert_equal "snapshots", command 614 | assert_equal 5, argv.size 615 | assert_equal "--config", argv[0] 616 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 617 | assert_equal "--parallelism", argv[2] 618 | assert_equal 10, argv[3] 619 | assert_equal "db/test.sqlite3", argv[4] 620 | end 621 | Litestream::Commands.stub :run, stub do 622 | Litestream::Commands.snapshots("db/test.sqlite3", "--parallelism" => 10) 623 | end 624 | end 625 | 626 | def test_snapshots_with_config_option 627 | stub = proc do |cmd| 628 | executable, command, *argv = cmd 629 | assert_match Regexp.new("exe/test/litestream"), executable 630 | assert_equal "snapshots", command 631 | assert_equal 3, argv.size 632 | assert_equal "--config", argv[0] 633 | assert_equal "CONFIG", argv[1] 634 | assert_equal "db/test.sqlite3", argv[2] 635 | end 636 | Litestream::Commands.stub :run, stub do 637 | Litestream::Commands.snapshots("db/test.sqlite3", "--config" => "CONFIG") 638 | end 639 | end 640 | 641 | def test_snapshots_sets_replica_bucket_env_var_from_config_when_env_var_not_set 642 | Litestream.replica_bucket = "mybkt" 643 | 644 | Litestream::Commands.stub :run, nil do 645 | Litestream::Commands.snapshots("db/test.sqlite3") 646 | end 647 | 648 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 649 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 650 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 651 | end 652 | 653 | def test_snapshots_sets_replica_key_id_env_var_from_config_when_env_var_not_set 654 | Litestream.replica_key_id = "mykey" 655 | 656 | Litestream::Commands.stub :run, nil do 657 | Litestream::Commands.snapshots("db/test.sqlite3") 658 | end 659 | 660 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 661 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 662 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 663 | end 664 | 665 | def test_snapshots_sets_replica_access_key_env_var_from_config_when_env_var_not_set 666 | Litestream.replica_access_key = "access" 667 | 668 | Litestream::Commands.stub :run, nil do 669 | Litestream::Commands.snapshots("db/test.sqlite3") 670 | end 671 | 672 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 673 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 674 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 675 | end 676 | 677 | def test_snapshots_sets_all_env_vars_from_config_when_env_vars_not_set 678 | Litestream.replica_bucket = "mybkt" 679 | Litestream.replica_key_id = "mykey" 680 | Litestream.replica_access_key = "access" 681 | 682 | Litestream::Commands.stub :run, nil do 683 | Litestream::Commands.snapshots("db/test.sqlite3") 684 | end 685 | 686 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 687 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 688 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 689 | end 690 | 691 | def test_snapshots_does_not_set_env_var_from_config_when_env_vars_already_set 692 | ENV["LITESTREAM_REPLICA_BUCKET"] = "original_bkt" 693 | ENV["LITESTREAM_ACCESS_KEY_ID"] = "original_key" 694 | ENV["LITESTREAM_SECRET_ACCESS_KEY"] = "original_access" 695 | 696 | Litestream.replica_bucket = "mybkt" 697 | Litestream.replica_key_id = "mykey" 698 | Litestream.replica_access_key = "access" 699 | 700 | Litestream::Commands.stub :run, nil do 701 | Litestream::Commands.snapshots("db/test.sqlite3") 702 | end 703 | 704 | assert_equal "original_bkt", ENV["LITESTREAM_REPLICA_BUCKET"] 705 | assert_equal "original_key", ENV["LITESTREAM_ACCESS_KEY_ID"] 706 | assert_equal "original_access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 707 | end 708 | end 709 | 710 | class TestWalCommand < TestCommands 711 | def test_wal_with_no_options 712 | stub = proc do |cmd| 713 | executable, command, *argv = cmd 714 | assert_match Regexp.new("exe/test/litestream"), executable 715 | assert_equal "wal", command 716 | assert_equal 3, argv.size 717 | assert_equal "--config", argv[0] 718 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 719 | assert_equal "db/test.sqlite3", argv[2] 720 | end 721 | Litestream::Commands.stub :run, stub do 722 | Litestream::Commands.wal("db/test.sqlite3") 723 | end 724 | end 725 | 726 | def test_wal_with_boolean_option 727 | stub = proc do |cmd| 728 | executable, command, *argv = cmd 729 | assert_match Regexp.new("exe/test/litestream"), executable 730 | assert_equal "wal", command 731 | assert_equal 4, argv.size 732 | assert_equal "--config", argv[0] 733 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 734 | assert_equal "--if-db-not-exists", argv[2] 735 | assert_equal "db/test.sqlite3", argv[3] 736 | end 737 | Litestream::Commands.stub :run, stub do 738 | Litestream::Commands.wal("db/test.sqlite3", "--if-db-not-exists" => nil) 739 | end 740 | end 741 | 742 | def test_wal_with_string_option 743 | stub = proc do |cmd| 744 | executable, command, *argv = cmd 745 | assert_match Regexp.new("exe/test/litestream"), executable 746 | assert_equal "wal", command 747 | assert_equal 5, argv.size 748 | assert_equal "--config", argv[0] 749 | assert_match Regexp.new("dummy/config/litestream.yml"), argv[1] 750 | assert_equal "--parallelism", argv[2] 751 | assert_equal 10, argv[3] 752 | assert_equal "db/test.sqlite3", argv[4] 753 | end 754 | Litestream::Commands.stub :run, stub do 755 | Litestream::Commands.wal("db/test.sqlite3", "--parallelism" => 10) 756 | end 757 | end 758 | 759 | def test_wal_with_config_option 760 | stub = proc do |cmd| 761 | executable, command, *argv = cmd 762 | assert_match Regexp.new("exe/test/litestream"), executable 763 | assert_equal "wal", command 764 | assert_equal 3, argv.size 765 | assert_equal "--config", argv[0] 766 | assert_equal "CONFIG", argv[1] 767 | assert_equal "db/test.sqlite3", argv[2] 768 | end 769 | Litestream::Commands.stub :run, stub do 770 | Litestream::Commands.wal("db/test.sqlite3", "--config" => "CONFIG") 771 | end 772 | end 773 | 774 | def test_wal_sets_replica_bucket_env_var_from_config_when_env_var_not_set 775 | Litestream.replica_bucket = "mybkt" 776 | 777 | Litestream::Commands.stub :run, nil do 778 | Litestream::Commands.wal("db/test.sqlite3") 779 | end 780 | 781 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 782 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 783 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 784 | end 785 | 786 | def test_wal_sets_replica_key_id_env_var_from_config_when_env_var_not_set 787 | Litestream.replica_key_id = "mykey" 788 | 789 | Litestream::Commands.stub :run, nil do 790 | Litestream::Commands.wal("db/test.sqlite3") 791 | end 792 | 793 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 794 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 795 | assert_nil ENV["LITESTREAM_SECRET_ACCESS_KEY"] 796 | end 797 | 798 | def test_wal_sets_replica_access_key_env_var_from_config_when_env_var_not_set 799 | Litestream.replica_access_key = "access" 800 | 801 | Litestream::Commands.stub :run, nil do 802 | Litestream::Commands.wal("db/test.sqlite3") 803 | end 804 | 805 | assert_nil ENV["LITESTREAM_REPLICA_BUCKET"] 806 | assert_nil ENV["LITESTREAM_ACCESS_KEY_ID"] 807 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 808 | end 809 | 810 | def test_wal_sets_all_env_vars_from_config_when_env_vars_not_set 811 | Litestream.replica_bucket = "mybkt" 812 | Litestream.replica_key_id = "mykey" 813 | Litestream.replica_access_key = "access" 814 | 815 | Litestream::Commands.stub :run, nil do 816 | Litestream::Commands.wal("db/test.sqlite3") 817 | end 818 | 819 | assert_equal "mybkt", ENV["LITESTREAM_REPLICA_BUCKET"] 820 | assert_equal "mykey", ENV["LITESTREAM_ACCESS_KEY_ID"] 821 | assert_equal "access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 822 | end 823 | 824 | def test_wal_does_not_set_env_var_from_config_when_env_vars_already_set 825 | ENV["LITESTREAM_REPLICA_BUCKET"] = "original_bkt" 826 | ENV["LITESTREAM_ACCESS_KEY_ID"] = "original_key" 827 | ENV["LITESTREAM_SECRET_ACCESS_KEY"] = "original_access" 828 | 829 | Litestream.replica_bucket = "mybkt" 830 | Litestream.replica_key_id = "mykey" 831 | Litestream.replica_access_key = "access" 832 | 833 | Litestream::Commands.stub :run, nil do 834 | Litestream::Commands.wal("db/test.sqlite3") 835 | end 836 | 837 | assert_equal "original_bkt", ENV["LITESTREAM_REPLICA_BUCKET"] 838 | assert_equal "original_key", ENV["LITESTREAM_ACCESS_KEY_ID"] 839 | assert_equal "original_access", ENV["LITESTREAM_SECRET_ACCESS_KEY"] 840 | end 841 | end 842 | 843 | class TestOutput < ActiveSupport::TestCase 844 | def test_output_formatting_generates_table_with_data 845 | data = [ 846 | {path: "/storage/database.db", replicas: "s3"}, 847 | {path: "/storage/another-database.db", replicas: "s3"} 848 | ] 849 | 850 | result = Litestream::Commands::Output.format(data) 851 | lines = result.split("\n") 852 | 853 | assert_equal 3, lines.length 854 | 855 | assert_includes lines[0], "path" 856 | assert_includes lines[0], "replicas" 857 | assert_includes lines[1], "/storage/database.db" 858 | assert_includes lines[2], "/storage/another-database.db" 859 | end 860 | 861 | def test_output_formatting_generates_formatted_table 862 | data = [ 863 | {path: "/storage/database.db", replicas: "s3"}, 864 | {path: "/storage/another-database.db", replicas: "s3"} 865 | ] 866 | 867 | result = Litestream::Commands::Output.format(data) 868 | lines = result.split("\n") 869 | 870 | replicas_pos = lines[0].index("replicas") 871 | assert_equal replicas_pos, lines[1].index("s3") 872 | assert_equal replicas_pos, lines[2].index("s3") 873 | end 874 | end 875 | end 876 | --------------------------------------------------------------------------------